botmux 2.88.0 → 2.89.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/dist/bot-registry.d.ts +2 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +4 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
- package/dist/cli/dashboard-endpoint.js +9 -3
- package/dist/cli/dashboard-endpoint.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +9 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-command/groups.d.ts +20 -0
- package/dist/core/dashboard-command/groups.d.ts.map +1 -0
- package/dist/core/dashboard-command/groups.js +52 -0
- package/dist/core/dashboard-command/groups.js.map +1 -0
- package/dist/core/dashboard-command/index.d.ts +30 -0
- package/dist/core/dashboard-command/index.d.ts.map +1 -0
- package/dist/core/dashboard-command/index.js +81 -0
- package/dist/core/dashboard-command/index.js.map +1 -0
- package/dist/core/dashboard-command/overview.d.ts +29 -0
- package/dist/core/dashboard-command/overview.d.ts.map +1 -0
- package/dist/core/dashboard-command/overview.js +61 -0
- package/dist/core/dashboard-command/overview.js.map +1 -0
- package/dist/core/dashboard-command/owner-gate.d.ts +41 -0
- package/dist/core/dashboard-command/owner-gate.d.ts.map +1 -0
- package/dist/core/dashboard-command/owner-gate.js +40 -0
- package/dist/core/dashboard-command/owner-gate.js.map +1 -0
- package/dist/core/dashboard-command/schedules.d.ts +20 -0
- package/dist/core/dashboard-command/schedules.d.ts.map +1 -0
- package/dist/core/dashboard-command/schedules.js +47 -0
- package/dist/core/dashboard-command/schedules.js.map +1 -0
- package/dist/core/dashboard-command/sessions.d.ts +22 -0
- package/dist/core/dashboard-command/sessions.d.ts.map +1 -0
- package/dist/core/dashboard-command/sessions.js +43 -0
- package/dist/core/dashboard-command/sessions.js.map +1 -0
- package/dist/core/dashboard-command/settings.d.ts +21 -0
- package/dist/core/dashboard-command/settings.d.ts.map +1 -0
- package/dist/core/dashboard-command/settings.js +55 -0
- package/dist/core/dashboard-command/settings.js.map +1 -0
- package/dist/core/dashboard-command/stub.d.ts +16 -0
- package/dist/core/dashboard-command/stub.d.ts.map +1 -0
- package/dist/core/dashboard-command/stub.js +28 -0
- package/dist/core/dashboard-command/stub.js.map +1 -0
- package/dist/core/dashboard-command/workflows.d.ts +20 -0
- package/dist/core/dashboard-command/workflows.d.ts.map +1 -0
- package/dist/core/dashboard-command/workflows.js +45 -0
- package/dist/core/dashboard-command/workflows.js.map +1 -0
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +1 -1
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/session-create.d.ts +0 -1
- package/dist/core/session-create.d.ts.map +1 -1
- package/dist/core/session-create.js +0 -3
- package/dist/core/session-create.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +3 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/daemon-internal-client-wrapper.d.ts +41 -0
- package/dist/daemon-internal-client-wrapper.d.ts.map +1 -0
- package/dist/daemon-internal-client-wrapper.js +65 -0
- package/dist/daemon-internal-client-wrapper.js.map +1 -0
- package/dist/dashboard/aggregator.d.ts +12 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +16 -0
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth.d.ts +7 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +21 -0
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/card-model-types.d.ts +50 -0
- package/dist/dashboard/card-model-types.d.ts.map +1 -0
- package/dist/dashboard/card-model-types.js +9 -0
- package/dist/dashboard/card-model-types.js.map +1 -0
- package/dist/dashboard/daemon-internal-api.d.ts +90 -0
- package/dist/dashboard/daemon-internal-api.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-api.js +534 -0
- package/dist/dashboard/daemon-internal-api.js.map +1 -0
- package/dist/dashboard/daemon-internal-auth.d.ts +124 -0
- package/dist/dashboard/daemon-internal-auth.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-auth.js +188 -0
- package/dist/dashboard/daemon-internal-auth.js.map +1 -0
- package/dist/dashboard/daemon-internal-client.d.ts +67 -0
- package/dist/dashboard/daemon-internal-client.d.ts.map +1 -0
- package/dist/dashboard/daemon-internal-client.js +157 -0
- package/dist/dashboard/daemon-internal-client.js.map +1 -0
- package/dist/dashboard/dashboard-admins.d.ts +8 -0
- package/dist/dashboard/dashboard-admins.d.ts.map +1 -0
- package/dist/dashboard/dashboard-admins.js +15 -0
- package/dist/dashboard/dashboard-admins.js.map +1 -0
- package/dist/dashboard/federation-spoke-api.d.ts +3 -0
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +11 -8
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/groups-action-helpers.d.ts +69 -0
- package/dist/dashboard/groups-action-helpers.d.ts.map +1 -0
- package/dist/dashboard/groups-action-helpers.js +141 -0
- package/dist/dashboard/groups-action-helpers.js.map +1 -0
- package/dist/dashboard/groups-card-model.d.ts +120 -0
- package/dist/dashboard/groups-card-model.d.ts.map +1 -0
- package/dist/dashboard/groups-card-model.js +175 -0
- package/dist/dashboard/groups-card-model.js.map +1 -0
- package/dist/dashboard/schedule-card-model.d.ts +172 -0
- package/dist/dashboard/schedule-card-model.d.ts.map +1 -0
- package/dist/dashboard/schedule-card-model.js +290 -0
- package/dist/dashboard/schedule-card-model.js.map +1 -0
- package/dist/dashboard/session-card-model.d.ts +83 -0
- package/dist/dashboard/session-card-model.d.ts.map +1 -0
- package/dist/dashboard/session-card-model.js +196 -0
- package/dist/dashboard/session-card-model.js.map +1 -0
- package/dist/dashboard/settings-card-model.d.ts +73 -0
- package/dist/dashboard/settings-card-model.d.ts.map +1 -0
- package/dist/dashboard/settings-card-model.js +94 -0
- package/dist/dashboard/settings-card-model.js.map +1 -0
- package/dist/dashboard/settings-owner-resolver.d.ts +32 -0
- package/dist/dashboard/settings-owner-resolver.d.ts.map +1 -0
- package/dist/dashboard/settings-owner-resolver.js +43 -0
- package/dist/dashboard/settings-owner-resolver.js.map +1 -0
- package/dist/dashboard/settings-write-applier.d.ts +84 -0
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -0
- package/dist/dashboard/settings-write-applier.js +119 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -0
- package/dist/dashboard/workflow-api.d.ts.map +1 -1
- package/dist/dashboard/workflow-api.js +38 -119
- package/dist/dashboard/workflow-api.js.map +1 -1
- package/dist/dashboard/workflow-card-model.d.ts +127 -0
- package/dist/dashboard/workflow-card-model.d.ts.map +1 -0
- package/dist/dashboard/workflow-card-model.js +186 -0
- package/dist/dashboard/workflow-card-model.js.map +1 -0
- package/dist/dashboard/workflows-action-helpers.d.ts +105 -0
- package/dist/dashboard/workflows-action-helpers.d.ts.map +1 -0
- package/dist/dashboard/workflows-action-helpers.js +196 -0
- package/dist/dashboard/workflows-action-helpers.js.map +1 -0
- package/dist/dashboard.js +196 -271
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +266 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +266 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -0
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +2 -1
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +54 -2
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +127 -1
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +22 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/groups-card.d.ts +90 -0
- package/dist/im/lark/groups-card.d.ts.map +1 -0
- package/dist/im/lark/groups-card.js +832 -0
- package/dist/im/lark/groups-card.js.map +1 -0
- package/dist/im/lark/overview-card.d.ts +101 -0
- package/dist/im/lark/overview-card.d.ts.map +1 -0
- package/dist/im/lark/overview-card.js +475 -0
- package/dist/im/lark/overview-card.js.map +1 -0
- package/dist/im/lark/schedules-card.d.ts +96 -0
- package/dist/im/lark/schedules-card.d.ts.map +1 -0
- package/dist/im/lark/schedules-card.js +892 -0
- package/dist/im/lark/schedules-card.js.map +1 -0
- package/dist/im/lark/sessions-card.d.ts +117 -0
- package/dist/im/lark/sessions-card.d.ts.map +1 -0
- package/dist/im/lark/sessions-card.js +870 -0
- package/dist/im/lark/sessions-card.js.map +1 -0
- package/dist/im/lark/settings-card.d.ts +125 -0
- package/dist/im/lark/settings-card.d.ts.map +1 -0
- package/dist/im/lark/settings-card.js +433 -0
- package/dist/im/lark/settings-card.js.map +1 -0
- package/dist/im/lark/workflows-card.d.ts +102 -0
- package/dist/im/lark/workflows-card.d.ts.map +1 -0
- package/dist/im/lark/workflows-card.js +737 -0
- package/dist/im/lark/workflows-card.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups dashboard card.
|
|
3
|
+
*
|
|
4
|
+
* List view + detail/manage view. The list stays compact (5/page); the
|
|
5
|
+
* detail card carries per-bot membership / oncall / role management actions.
|
|
6
|
+
*
|
|
7
|
+
* Global dashboard scope: `/dashboard` renders the full groups matrix. The
|
|
8
|
+
* row view summarizes coverage across all bot columns (joined/total) instead
|
|
9
|
+
* of pretending the matrix has a single caller-bot column.
|
|
10
|
+
*
|
|
11
|
+
* Security:
|
|
12
|
+
* - `invokerOpenId` is the invoking admin's `ou_*` (invoker-lock anchor).
|
|
13
|
+
* - Admin gate runs at the command entry AND on every callback.
|
|
14
|
+
* - sender union_id NEVER lands on action.value.
|
|
15
|
+
*
|
|
16
|
+
* Response: success returns `{ card }` only (no toast) — single-pass render,
|
|
17
|
+
* no stale-frame flash. Errors / permission denials return `{ toast }`.
|
|
18
|
+
*
|
|
19
|
+
* Sort order: keep model output order verbatim; resorting in the card would
|
|
20
|
+
* silently diverge from the Web Dashboard.
|
|
21
|
+
*/
|
|
22
|
+
import { isDashboardAdmin } from '../../dashboard/dashboard-admins.js';
|
|
23
|
+
import { buildGroupDetail, buildGroupRow } from '../../dashboard/groups-card-model.js';
|
|
24
|
+
import { t } from '../../i18n/index.js';
|
|
25
|
+
export const GROUPS_ACTION_REFRESH = 'dash_groups_refresh';
|
|
26
|
+
export const GROUPS_ACTION_PAGE = 'dash_groups_page';
|
|
27
|
+
export const GROUPS_ACTION_DETAIL = 'dash_groups_detail';
|
|
28
|
+
export const GROUPS_ACTION_BACK_TO_LIST = 'dash_groups_back_to_list';
|
|
29
|
+
export const GROUPS_ACTION_ADD_BOT = 'dash_groups_add_bot';
|
|
30
|
+
export const GROUPS_ACTION_LEAVE_BOT = 'dash_groups_leave_bot';
|
|
31
|
+
export const GROUPS_ACTION_ONCALL_BIND = 'dash_groups_oncall_bind';
|
|
32
|
+
export const GROUPS_ACTION_ONCALL_UNBIND = 'dash_groups_oncall_unbind';
|
|
33
|
+
export const GROUPS_ACTION_ROLE_OPEN = 'dash_groups_role_open';
|
|
34
|
+
export const GROUPS_ACTION_ROLE_SAVE = 'dash_groups_role_save';
|
|
35
|
+
export const GROUPS_ACTION_ROLE_DELETE = 'dash_groups_role_delete';
|
|
36
|
+
/** Action emitted by the "🔙 返回总览" button on overview-origin sub-cards.
|
|
37
|
+
* Same string as overview-card's OVERVIEW_ACTION_REFRESH (avoids a circular
|
|
38
|
+
* import). card-handler routes by action prefix, so dispatch lands on the
|
|
39
|
+
* overview handler regardless of which sub-card emitted it. */
|
|
40
|
+
const BACK_TO_OVERVIEW_ACTION = 'dash_overview_refresh';
|
|
41
|
+
/** Default page size for standalone and overview-drilldown list cards. */
|
|
42
|
+
const PAGE_SIZE = 5;
|
|
43
|
+
const MAX_PAGE_SIZE = 100;
|
|
44
|
+
/** Hard cap on `select_static` option count for the "jump to page" picker.
|
|
45
|
+
* Lark caps select options around this; we also keep payload small. Above
|
|
46
|
+
* the cap we fall back to prev/next only. */
|
|
47
|
+
const JUMP_PAGE_MAX_OPTIONS = 50;
|
|
48
|
+
/** Mapping from coverage status to a stable colour-emoji prefix. Pure. */
|
|
49
|
+
function statusIcon(status) {
|
|
50
|
+
switch (status) {
|
|
51
|
+
case 'in': return '🟢';
|
|
52
|
+
case 'out': return '⚪';
|
|
53
|
+
case 'unknown': return '🟡';
|
|
54
|
+
case 'error': return '🔴';
|
|
55
|
+
default: return '⚫';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Translate coverage status to its localized label. */
|
|
59
|
+
function statusLabel(status, locale) {
|
|
60
|
+
switch (status) {
|
|
61
|
+
case 'in': return t('card.dashboard.groups.status.in', undefined, locale);
|
|
62
|
+
case 'out': return t('card.dashboard.groups.status.out', undefined, locale);
|
|
63
|
+
case 'unknown': return t('card.dashboard.groups.status.unknown', undefined, locale);
|
|
64
|
+
case 'error': return t('card.dashboard.groups.status.error', undefined, locale);
|
|
65
|
+
default: return status;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function clampPageSize(pageSize) {
|
|
69
|
+
if (typeof pageSize !== 'number' || !Number.isFinite(pageSize) || pageSize <= 0)
|
|
70
|
+
return PAGE_SIZE;
|
|
71
|
+
return Math.min(Math.floor(pageSize), MAX_PAGE_SIZE);
|
|
72
|
+
}
|
|
73
|
+
function buildNavFields(opts) {
|
|
74
|
+
const navFields = {};
|
|
75
|
+
const effectivePageSize = clampPageSize(opts.pageSize);
|
|
76
|
+
if (opts.origin === 'overview')
|
|
77
|
+
navFields.origin = 'overview';
|
|
78
|
+
if (effectivePageSize !== PAGE_SIZE)
|
|
79
|
+
navFields.page_size = String(effectivePageSize);
|
|
80
|
+
if (opts.scope === 'global')
|
|
81
|
+
navFields.dashboard_scope = 'global';
|
|
82
|
+
return navFields;
|
|
83
|
+
}
|
|
84
|
+
/** Build the groups list card JSON. Pure (composes + paginates + renders). */
|
|
85
|
+
export function buildGroupsCard(matrix, opts) {
|
|
86
|
+
const effectivePageSize = clampPageSize(opts.pageSize);
|
|
87
|
+
// Project EVERY chat into a row DTO ourselves rather than going through
|
|
88
|
+
// `buildGroupRows`, because the pipeline helper also paginates and would
|
|
89
|
+
// silently clip the card list. The model owns the
|
|
90
|
+
// canonical sort — we walk `matrix.chats` verbatim, no client re-sort.
|
|
91
|
+
const allRows = matrix.chats.map(c => buildGroupRow(c, matrix.bots));
|
|
92
|
+
// Header counts before pagination.
|
|
93
|
+
const total = allRows.length;
|
|
94
|
+
const joined = allRows.reduce((n, r) => (aggregateCoverageStatus(r.coverage) === 'in' ? n + 1 : n), 0);
|
|
95
|
+
const missing = total - joined;
|
|
96
|
+
// Paginate the DTO list (mirror paginateGroups semantics on the projected rows).
|
|
97
|
+
const totalPages = Math.max(1, Math.ceil(total / effectivePageSize));
|
|
98
|
+
let activePage = Number.isFinite(opts.page) ? Math.floor(opts.page) : 1;
|
|
99
|
+
if (activePage < 1)
|
|
100
|
+
activePage = 1;
|
|
101
|
+
if (activePage > totalPages)
|
|
102
|
+
activePage = totalPages;
|
|
103
|
+
const start = (activePage - 1) * effectivePageSize;
|
|
104
|
+
const pageItems = allRows.slice(start, start + effectivePageSize);
|
|
105
|
+
// Plumb origin + page_size + scope into every button.value so refresh/page
|
|
106
|
+
// rebuilds keep the same dashboard context.
|
|
107
|
+
const navFields = buildNavFields({ pageSize: effectivePageSize, origin: opts.origin, scope: opts.scope });
|
|
108
|
+
const elements = [];
|
|
109
|
+
// Sub-header — counts + page indicator.
|
|
110
|
+
elements.push({
|
|
111
|
+
tag: 'div',
|
|
112
|
+
text: {
|
|
113
|
+
tag: 'lark_md',
|
|
114
|
+
content: t('card.dashboard.groups.count_summary', {
|
|
115
|
+
total: String(total),
|
|
116
|
+
joined: String(joined),
|
|
117
|
+
missing: String(missing),
|
|
118
|
+
page: String(activePage),
|
|
119
|
+
totalPages: String(totalPages),
|
|
120
|
+
}, opts.locale),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
elements.push({ tag: 'hr' });
|
|
124
|
+
if (pageItems.length === 0) {
|
|
125
|
+
elements.push({
|
|
126
|
+
tag: 'div',
|
|
127
|
+
text: {
|
|
128
|
+
tag: 'lark_md',
|
|
129
|
+
content: t('card.dashboard.groups.empty', undefined, opts.locale),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
for (const row of pageItems) {
|
|
135
|
+
elements.push(...renderRow(row, opts.locale, {
|
|
136
|
+
invokerOpenId: opts.invokerOpenId,
|
|
137
|
+
page: activePage,
|
|
138
|
+
navFields,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
elements.push({ tag: 'hr' });
|
|
143
|
+
// Pagination + refresh.
|
|
144
|
+
const actions = [];
|
|
145
|
+
if (totalPages > 1) {
|
|
146
|
+
actions.push({
|
|
147
|
+
tag: 'button',
|
|
148
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.prev', undefined, opts.locale) },
|
|
149
|
+
type: 'default',
|
|
150
|
+
disabled: activePage <= 1,
|
|
151
|
+
value: {
|
|
152
|
+
action: GROUPS_ACTION_PAGE,
|
|
153
|
+
invoker_open_id: opts.invokerOpenId,
|
|
154
|
+
page: String(Math.max(1, activePage - 1)),
|
|
155
|
+
...navFields,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
actions.push({
|
|
159
|
+
tag: 'button',
|
|
160
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.next', undefined, opts.locale) },
|
|
161
|
+
type: 'default',
|
|
162
|
+
disabled: activePage >= totalPages,
|
|
163
|
+
value: {
|
|
164
|
+
action: GROUPS_ACTION_PAGE,
|
|
165
|
+
invoker_open_id: opts.invokerOpenId,
|
|
166
|
+
page: String(Math.min(totalPages, activePage + 1)),
|
|
167
|
+
...navFields,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
// "Jump to page" select — same action as prev/next, page comes via
|
|
171
|
+
// action.option instead of value.page. Handler reads `value.page ??
|
|
172
|
+
// action.option ?? '1'` so both paths converge on one branch. Capped at
|
|
173
|
+
// JUMP_PAGE_MAX_OPTIONS to keep payload small / inside Lark's option
|
|
174
|
+
// limit (above the cap, prev/next still works).
|
|
175
|
+
if (totalPages > 2 && totalPages <= JUMP_PAGE_MAX_OPTIONS) {
|
|
176
|
+
const options = Array.from({ length: totalPages }, (_, i) => {
|
|
177
|
+
const n = i + 1;
|
|
178
|
+
return {
|
|
179
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.jump_page', { n: String(n), total: String(totalPages) }, opts.locale) },
|
|
180
|
+
value: String(n),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
actions.push({
|
|
184
|
+
tag: 'select_static',
|
|
185
|
+
placeholder: {
|
|
186
|
+
tag: 'plain_text',
|
|
187
|
+
content: t('card.dashboard.groups.jump_page', { n: String(activePage), total: String(totalPages) }, opts.locale),
|
|
188
|
+
},
|
|
189
|
+
initial_option: String(activePage),
|
|
190
|
+
options,
|
|
191
|
+
value: {
|
|
192
|
+
action: GROUPS_ACTION_PAGE,
|
|
193
|
+
invoker_open_id: opts.invokerOpenId,
|
|
194
|
+
...navFields,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
actions.push({
|
|
200
|
+
tag: 'button',
|
|
201
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.refresh', undefined, opts.locale) },
|
|
202
|
+
type: 'default',
|
|
203
|
+
value: {
|
|
204
|
+
action: GROUPS_ACTION_REFRESH,
|
|
205
|
+
invoker_open_id: opts.invokerOpenId,
|
|
206
|
+
...navFields,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// Overview drilldown only — "🔙 返回总览" reuses the overview-refresh
|
|
210
|
+
// action; card-handler routes by action prefix, so dispatch lands on
|
|
211
|
+
// overview-card.ts which rebuilds the parent card cleanly.
|
|
212
|
+
if (opts.origin === 'overview') {
|
|
213
|
+
actions.push({
|
|
214
|
+
tag: 'button',
|
|
215
|
+
text: { tag: 'plain_text', content: t('card.dashboard.overview.back_button', undefined, opts.locale) },
|
|
216
|
+
type: 'default',
|
|
217
|
+
value: {
|
|
218
|
+
action: BACK_TO_OVERVIEW_ACTION,
|
|
219
|
+
invoker_open_id: opts.invokerOpenId,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
elements.push({ tag: 'action', actions });
|
|
224
|
+
elements.push({
|
|
225
|
+
tag: 'note',
|
|
226
|
+
elements: [
|
|
227
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
return JSON.stringify({
|
|
231
|
+
config: { wide_screen_mode: true },
|
|
232
|
+
header: {
|
|
233
|
+
title: { tag: 'plain_text', content: t('card.dashboard.groups.title', undefined, opts.locale) },
|
|
234
|
+
template: 'blue',
|
|
235
|
+
},
|
|
236
|
+
elements,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
export function buildGroupsDetailCard(matrix, chat, opts) {
|
|
240
|
+
const detail = buildGroupDetail(chat, matrix.bots);
|
|
241
|
+
const displayName = detail.name && detail.name !== detail.chatId
|
|
242
|
+
? detail.name
|
|
243
|
+
: t('card.dashboard.groups.unnamed', undefined, opts.locale);
|
|
244
|
+
const navFields = buildNavFields({ pageSize: opts.pageSize, origin: opts.origin, scope: opts.scope });
|
|
245
|
+
const backValue = {
|
|
246
|
+
action: GROUPS_ACTION_BACK_TO_LIST,
|
|
247
|
+
invoker_open_id: opts.invokerOpenId,
|
|
248
|
+
...(opts.page ? { page: String(opts.page) } : {}),
|
|
249
|
+
...navFields,
|
|
250
|
+
};
|
|
251
|
+
const elements = [
|
|
252
|
+
{
|
|
253
|
+
tag: 'div',
|
|
254
|
+
text: {
|
|
255
|
+
tag: 'lark_md',
|
|
256
|
+
content: `**${escapeLarkMd(displayName)}**` +
|
|
257
|
+
`\n<font color="grey">${escapeLarkMd(detail.chatId)} · ${escapeLarkMd(t('card.dashboard.groups.joined_ratio', {
|
|
258
|
+
joined: String(detail.members.filter(m => m.status === 'in').length),
|
|
259
|
+
total: String(detail.members.length),
|
|
260
|
+
}, opts.locale))}</font>`,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{ tag: 'hr' },
|
|
264
|
+
];
|
|
265
|
+
for (const member of detail.members) {
|
|
266
|
+
elements.push(...renderDetailMember(detail.chatId, member, opts, navFields));
|
|
267
|
+
}
|
|
268
|
+
elements.push({ tag: 'hr' });
|
|
269
|
+
const footerActions = [{
|
|
270
|
+
tag: 'button',
|
|
271
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.back', undefined, opts.locale) },
|
|
272
|
+
type: 'default',
|
|
273
|
+
value: backValue,
|
|
274
|
+
}];
|
|
275
|
+
if (opts.origin === 'overview') {
|
|
276
|
+
footerActions.push({
|
|
277
|
+
tag: 'button',
|
|
278
|
+
text: { tag: 'plain_text', content: t('card.dashboard.overview.back_button', undefined, opts.locale) },
|
|
279
|
+
type: 'default',
|
|
280
|
+
value: { action: BACK_TO_OVERVIEW_ACTION, invoker_open_id: opts.invokerOpenId },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
elements.push({ tag: 'action', actions: footerActions });
|
|
284
|
+
elements.push({
|
|
285
|
+
tag: 'note',
|
|
286
|
+
elements: [
|
|
287
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
return JSON.stringify({
|
|
291
|
+
config: { wide_screen_mode: true },
|
|
292
|
+
header: {
|
|
293
|
+
title: { tag: 'plain_text', content: t('card.dashboard.groups.detail.title', undefined, opts.locale) },
|
|
294
|
+
template: 'blue',
|
|
295
|
+
},
|
|
296
|
+
elements,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function renderDetailMember(chatId, member, opts, navFields) {
|
|
300
|
+
const status = member.status;
|
|
301
|
+
const icon = statusIcon(status);
|
|
302
|
+
const roleLabel = member.hasRole
|
|
303
|
+
? t('card.dashboard.groups.role_configured', undefined, opts.locale)
|
|
304
|
+
: t('card.dashboard.groups.role_empty', undefined, opts.locale);
|
|
305
|
+
const oncallLabel = member.oncallWorkingDir !== null
|
|
306
|
+
? t('card.dashboard.groups.oncall_enabled', { workingDir: member.oncallWorkingDir || '-' }, opts.locale)
|
|
307
|
+
: t('card.dashboard.groups.oncall_disabled', undefined, opts.locale);
|
|
308
|
+
const secondary = [
|
|
309
|
+
statusLabel(status, opts.locale),
|
|
310
|
+
roleLabel,
|
|
311
|
+
oncallLabel,
|
|
312
|
+
member.isOwnerBot ? t('card.dashboard.groups.owner_bot', undefined, opts.locale) : undefined,
|
|
313
|
+
].filter((x) => typeof x === 'string' && x.length > 0);
|
|
314
|
+
const valueBase = {
|
|
315
|
+
invoker_open_id: opts.invokerOpenId,
|
|
316
|
+
chat_id: chatId,
|
|
317
|
+
app_id: member.larkAppId,
|
|
318
|
+
...(opts.page ? { page: String(opts.page) } : {}),
|
|
319
|
+
...navFields,
|
|
320
|
+
};
|
|
321
|
+
const actions = [];
|
|
322
|
+
const extraElements = [];
|
|
323
|
+
if (status === 'in') {
|
|
324
|
+
actions.push({
|
|
325
|
+
tag: 'button',
|
|
326
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.role', undefined, opts.locale) },
|
|
327
|
+
type: 'default',
|
|
328
|
+
value: { action: GROUPS_ACTION_ROLE_OPEN, ...valueBase },
|
|
329
|
+
});
|
|
330
|
+
if (member.oncallWorkingDir !== null) {
|
|
331
|
+
actions.push({
|
|
332
|
+
tag: 'button',
|
|
333
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.oncall_unbind', undefined, opts.locale) },
|
|
334
|
+
type: 'danger',
|
|
335
|
+
value: { action: GROUPS_ACTION_ONCALL_UNBIND, ...valueBase },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
extraElements.push({
|
|
340
|
+
tag: 'form',
|
|
341
|
+
name: 'groups_oncall_form',
|
|
342
|
+
elements: [
|
|
343
|
+
{
|
|
344
|
+
tag: 'input',
|
|
345
|
+
name: 'working_dir',
|
|
346
|
+
placeholder: {
|
|
347
|
+
tag: 'plain_text',
|
|
348
|
+
content: t('card.dashboard.groups.working_dir_placeholder', undefined, opts.locale),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
tag: 'button',
|
|
353
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.oncall_bind', undefined, opts.locale) },
|
|
354
|
+
type: 'primary',
|
|
355
|
+
name: 'groups_oncall_bind',
|
|
356
|
+
action_type: 'form_submit',
|
|
357
|
+
value: { action: GROUPS_ACTION_ONCALL_BIND, ...valueBase },
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
actions.push({
|
|
363
|
+
tag: 'button',
|
|
364
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.leave_bot', undefined, opts.locale) },
|
|
365
|
+
type: 'danger',
|
|
366
|
+
confirm: {
|
|
367
|
+
title: { tag: 'plain_text', content: t('card.dashboard.groups.confirm.leave.title', undefined, opts.locale) },
|
|
368
|
+
text: {
|
|
369
|
+
tag: 'plain_text',
|
|
370
|
+
content: t('card.dashboard.groups.confirm.leave.text', { bot: member.botName }, opts.locale),
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
value: { action: GROUPS_ACTION_LEAVE_BOT, ...valueBase },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
actions.push({
|
|
378
|
+
tag: 'button',
|
|
379
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.add_bot', undefined, opts.locale) },
|
|
380
|
+
type: 'primary',
|
|
381
|
+
value: { action: GROUPS_ACTION_ADD_BOT, ...valueBase },
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
tag: 'div',
|
|
387
|
+
text: {
|
|
388
|
+
tag: 'lark_md',
|
|
389
|
+
content: `${icon} **${escapeLarkMd(member.botName)}** ` +
|
|
390
|
+
`<font color="grey">${escapeLarkMd(member.larkAppId.slice(-6))}</font>` +
|
|
391
|
+
`\n<font color="grey">${escapeLarkMd(secondary.join(' · '))}</font>`,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
tag: 'action',
|
|
396
|
+
actions,
|
|
397
|
+
},
|
|
398
|
+
...extraElements,
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
export function buildGroupsRoleCard(chat, member, roleContent, opts) {
|
|
402
|
+
const displayName = chat.name && chat.name !== chat.chatId
|
|
403
|
+
? chat.name
|
|
404
|
+
: t('card.dashboard.groups.unnamed', undefined, opts.locale);
|
|
405
|
+
const navFields = buildNavFields({ pageSize: opts.pageSize, origin: opts.origin, scope: opts.scope });
|
|
406
|
+
const valueBase = {
|
|
407
|
+
invoker_open_id: opts.invokerOpenId,
|
|
408
|
+
chat_id: chat.chatId,
|
|
409
|
+
app_id: member.larkAppId,
|
|
410
|
+
...(opts.page ? { page: String(opts.page) } : {}),
|
|
411
|
+
...navFields,
|
|
412
|
+
};
|
|
413
|
+
const elements = [
|
|
414
|
+
{
|
|
415
|
+
tag: 'div',
|
|
416
|
+
text: {
|
|
417
|
+
tag: 'lark_md',
|
|
418
|
+
content: `**${escapeLarkMd(displayName)} · ${escapeLarkMd(member.botName)}**` +
|
|
419
|
+
`\n<font color="grey">${escapeLarkMd(chat.chatId)} · ${escapeLarkMd(member.larkAppId)}</font>`,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{ tag: 'hr' },
|
|
423
|
+
{
|
|
424
|
+
tag: 'div',
|
|
425
|
+
text: {
|
|
426
|
+
tag: 'lark_md',
|
|
427
|
+
content: `**${t('card.dashboard.groups.current_role', undefined, opts.locale)}**` +
|
|
428
|
+
`\n${roleContent.trim().length > 0
|
|
429
|
+
? escapeLarkMd(roleContent)
|
|
430
|
+
: `_${escapeLarkMd(t('card.dashboard.groups.current_role_empty', undefined, opts.locale))}_`}`,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{ tag: 'hr' },
|
|
434
|
+
{
|
|
435
|
+
tag: 'div',
|
|
436
|
+
text: {
|
|
437
|
+
tag: 'lark_md',
|
|
438
|
+
content: `**${t('card.dashboard.groups.edit_role', undefined, opts.locale)}**`,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
tag: 'form',
|
|
443
|
+
name: 'groups_role_form',
|
|
444
|
+
elements: [
|
|
445
|
+
{
|
|
446
|
+
tag: 'input',
|
|
447
|
+
name: 'role',
|
|
448
|
+
default_value: roleContent,
|
|
449
|
+
placeholder: { tag: 'plain_text', content: t('card.dashboard.groups.role_placeholder', undefined, opts.locale) },
|
|
450
|
+
input_type: 'multiline_text',
|
|
451
|
+
rows: 8,
|
|
452
|
+
max_rows: 12,
|
|
453
|
+
auto_resize: true,
|
|
454
|
+
width: 'fill',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
tag: 'button',
|
|
458
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.role_save', undefined, opts.locale) },
|
|
459
|
+
type: 'primary',
|
|
460
|
+
name: 'groups_role_save',
|
|
461
|
+
action_type: 'form_submit',
|
|
462
|
+
value: { action: GROUPS_ACTION_ROLE_SAVE, ...valueBase },
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
tag: 'action',
|
|
468
|
+
actions: [
|
|
469
|
+
{
|
|
470
|
+
tag: 'button',
|
|
471
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.role_delete', undefined, opts.locale) },
|
|
472
|
+
type: 'danger',
|
|
473
|
+
disabled: roleContent.trim().length === 0,
|
|
474
|
+
value: { action: GROUPS_ACTION_ROLE_DELETE, ...valueBase },
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
tag: 'button',
|
|
478
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.btn.back', undefined, opts.locale) },
|
|
479
|
+
type: 'default',
|
|
480
|
+
value: { action: GROUPS_ACTION_DETAIL, ...valueBase },
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
return JSON.stringify({
|
|
486
|
+
config: { wide_screen_mode: true },
|
|
487
|
+
header: {
|
|
488
|
+
title: { tag: 'plain_text', content: t('card.dashboard.groups.role.title', undefined, opts.locale) },
|
|
489
|
+
template: 'blue',
|
|
490
|
+
},
|
|
491
|
+
elements,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function renderRow(row, locale, ctx) {
|
|
495
|
+
const status = aggregateCoverageStatus(row.coverage);
|
|
496
|
+
const icon = statusIcon(status);
|
|
497
|
+
// Primary: status icon + bold name (or unnamed fallback) + grey id suffix.
|
|
498
|
+
const displayName = row.name && row.name !== row.chatId
|
|
499
|
+
? row.name
|
|
500
|
+
: t('card.dashboard.groups.unnamed', undefined, locale);
|
|
501
|
+
const idSuffix = row.chatIdSuffix
|
|
502
|
+
? ` <font color="grey">${escapeLarkMd(row.chatIdSuffix)}</font>`
|
|
503
|
+
: '';
|
|
504
|
+
// Secondary: aggregate coverage across all bot columns.
|
|
505
|
+
const secondaryParts = [
|
|
506
|
+
t('card.dashboard.groups.coverage_label', { status: statusLabel(status, locale) }, locale),
|
|
507
|
+
t('card.dashboard.groups.joined_ratio', { joined: String(row.totalBots - row.missingCount), total: String(row.totalBots) }, locale),
|
|
508
|
+
];
|
|
509
|
+
const secondary = `\n<font color="grey">${escapeLarkMd(secondaryParts.join(' · '))}</font>`;
|
|
510
|
+
return [
|
|
511
|
+
{
|
|
512
|
+
tag: 'div',
|
|
513
|
+
text: {
|
|
514
|
+
tag: 'lark_md',
|
|
515
|
+
content: `${icon} **${escapeLarkMd(displayName)}**${idSuffix}` + secondary,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
tag: 'action',
|
|
520
|
+
actions: [{
|
|
521
|
+
tag: 'button',
|
|
522
|
+
text: { tag: 'plain_text', content: t('card.dashboard.groups.row_manage', undefined, locale) },
|
|
523
|
+
type: 'default',
|
|
524
|
+
value: {
|
|
525
|
+
action: GROUPS_ACTION_DETAIL,
|
|
526
|
+
invoker_open_id: ctx.invokerOpenId,
|
|
527
|
+
chat_id: row.chatId,
|
|
528
|
+
page: String(ctx.page),
|
|
529
|
+
...ctx.navFields,
|
|
530
|
+
},
|
|
531
|
+
}],
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
function aggregateCoverageStatus(cells) {
|
|
536
|
+
if (cells.length === 0)
|
|
537
|
+
return 'unknown';
|
|
538
|
+
if (cells.some(c => c.status === 'error'))
|
|
539
|
+
return 'error';
|
|
540
|
+
if (cells.some(c => c.status === 'unknown'))
|
|
541
|
+
return 'unknown';
|
|
542
|
+
if (cells.every(c => c.status === 'in'))
|
|
543
|
+
return 'in';
|
|
544
|
+
return 'out';
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Sanitize chat name / chatIdSuffix / workingDir for lark_md inclusion.
|
|
548
|
+
*
|
|
549
|
+
* Chat names are user-controlled (group titles), chatIdSuffix is bot-supplied
|
|
550
|
+
* but echoed near user content, and workingDir comes from the filesystem. All
|
|
551
|
+
* three flow into a `<font color="grey">…</font>` wrapper; without escaping,
|
|
552
|
+
* a payload like `</font><at id=ou_x></at>` would close our wrapper and
|
|
553
|
+
* inject @mention-looking content. Order matters: `&` first so a later `<`
|
|
554
|
+
* → `<` doesn't get re-encoded as `&lt;`.
|
|
555
|
+
*/
|
|
556
|
+
function escapeLarkMd(text) {
|
|
557
|
+
return text
|
|
558
|
+
.replace(/&/g, '&')
|
|
559
|
+
.replace(/</g, '<')
|
|
560
|
+
.replace(/>/g, '>')
|
|
561
|
+
.replace(/([*_~`])/g, '\\$1');
|
|
562
|
+
}
|
|
563
|
+
function ackToast(textKey, locale) {
|
|
564
|
+
return { toast: { type: 'info', content: t(textKey, undefined, locale) } };
|
|
565
|
+
}
|
|
566
|
+
function errorToast(textKey, params, locale) {
|
|
567
|
+
return { toast: { type: 'error', content: t(textKey, params, locale) } };
|
|
568
|
+
}
|
|
569
|
+
function cardResult(cardJson) {
|
|
570
|
+
return {
|
|
571
|
+
card: { type: 'raw', data: JSON.parse(cardJson) },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function matrixFromBody(body) {
|
|
575
|
+
const b = body ?? {};
|
|
576
|
+
return {
|
|
577
|
+
chats: b.chats ?? [],
|
|
578
|
+
bots: b.bots ?? [],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function loadGroupsMatrix(client, pathSuffix, locale) {
|
|
582
|
+
let r;
|
|
583
|
+
try {
|
|
584
|
+
r = await client.request({ method: 'GET', path: `/__daemon/groups-matrix${pathSuffix}` });
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
result: errorToast('card.dashboard.groups.list_failed', { reason: e.message }, locale),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (r.status !== 200) {
|
|
593
|
+
const reason = String(r.body?.error ?? `http_${r.status}`);
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
result: errorToast('card.dashboard.groups.list_failed', { reason }, locale),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return { ok: true, matrix: matrixFromBody(r.body) };
|
|
600
|
+
}
|
|
601
|
+
function findChat(matrix, chatId) {
|
|
602
|
+
if (!chatId)
|
|
603
|
+
return undefined;
|
|
604
|
+
return matrix.chats.find(c => c.chatId === chatId);
|
|
605
|
+
}
|
|
606
|
+
function findDetailMember(matrix, chat, appId) {
|
|
607
|
+
if (!appId)
|
|
608
|
+
return undefined;
|
|
609
|
+
return buildGroupDetail(chat, matrix.bots).members.find(m => m.larkAppId === appId);
|
|
610
|
+
}
|
|
611
|
+
function formValue(data, key) {
|
|
612
|
+
const raw = data.action?.form_value?.[key] ?? (key === 'role' ? data.action?.input_value : undefined);
|
|
613
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
614
|
+
}
|
|
615
|
+
function actionFailureReason(body, status) {
|
|
616
|
+
const b = body;
|
|
617
|
+
if (b?.ok === false && typeof b.error === 'string')
|
|
618
|
+
return b.error;
|
|
619
|
+
if (typeof b?.error === 'string')
|
|
620
|
+
return b.error;
|
|
621
|
+
if (Array.isArray(b?.result)) {
|
|
622
|
+
const failed = b.result.find((x) => x?.ok === false);
|
|
623
|
+
if (failed?.error)
|
|
624
|
+
return String(failed.error);
|
|
625
|
+
}
|
|
626
|
+
return `http_${status}`;
|
|
627
|
+
}
|
|
628
|
+
function bodyOk(body) {
|
|
629
|
+
const b = body;
|
|
630
|
+
if (b?.ok === false)
|
|
631
|
+
return false;
|
|
632
|
+
if (Array.isArray(b?.result))
|
|
633
|
+
return b.result.every((x) => x?.ok !== false);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
async function writeGroupAction(client, method, path, body, locale) {
|
|
637
|
+
let r;
|
|
638
|
+
try {
|
|
639
|
+
r = await client.request({ method, path, body });
|
|
640
|
+
}
|
|
641
|
+
catch (e) {
|
|
642
|
+
return errorToast('card.dashboard.groups.action_failed', { reason: e.message }, locale);
|
|
643
|
+
}
|
|
644
|
+
if (r.status >= 400 || !bodyOk(r.body)) {
|
|
645
|
+
return errorToast('card.dashboard.groups.action_failed', { reason: actionFailureReason(r.body, r.status) }, locale);
|
|
646
|
+
}
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
/** Dispatch a `dash_groups_*` action callback. Mirrors sessions-card. */
|
|
650
|
+
export async function handleGroupsCardAction(data, larkAppId, deps) {
|
|
651
|
+
const locale = deps.locale ?? 'zh';
|
|
652
|
+
const value = (data.action?.value ?? {});
|
|
653
|
+
const operatorOpenId = data.operator?.open_id;
|
|
654
|
+
const action = value.action;
|
|
655
|
+
// Invoker lock — fail-closed.
|
|
656
|
+
const invokerOpenId = value.invoker_open_id;
|
|
657
|
+
if (typeof invokerOpenId !== 'string' || !invokerOpenId) {
|
|
658
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
659
|
+
}
|
|
660
|
+
if (typeof operatorOpenId !== 'string' || !operatorOpenId) {
|
|
661
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
662
|
+
}
|
|
663
|
+
if (invokerOpenId !== operatorOpenId) {
|
|
664
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
665
|
+
}
|
|
666
|
+
// Per-bot admin gate.
|
|
667
|
+
if (!isDashboardAdmin(larkAppId, operatorOpenId, deps)) {
|
|
668
|
+
return ackToast('card.dashboard.settings.owner_only', locale);
|
|
669
|
+
}
|
|
670
|
+
// ─── Nav state (overview drilldown) ─────────────────────────────────
|
|
671
|
+
// Threaded by buildGroupsCard onto every button.value; we parse here so
|
|
672
|
+
// the rebuild path keeps the same shape (origin + page_size persist
|
|
673
|
+
// across refresh/page round-trips).
|
|
674
|
+
const navOrigin = value.origin === 'overview' ? 'overview' : undefined;
|
|
675
|
+
const parsedPageSize = Number.parseInt(value.page_size ?? '', 10);
|
|
676
|
+
const navPageSize = Number.isFinite(parsedPageSize) && parsedPageSize > 0 ? parsedPageSize : undefined;
|
|
677
|
+
const navScope = value.dashboard_scope === 'global' ? 'global' : undefined;
|
|
678
|
+
const pathSuffix = navScope === 'global' ? '?scope=global' : '';
|
|
679
|
+
const navOptsBase = {
|
|
680
|
+
invokerOpenId: operatorOpenId,
|
|
681
|
+
locale,
|
|
682
|
+
pageSize: navPageSize,
|
|
683
|
+
origin: navOrigin,
|
|
684
|
+
scope: navScope,
|
|
685
|
+
};
|
|
686
|
+
const chatId = value.chat_id;
|
|
687
|
+
const appId = value.app_id;
|
|
688
|
+
// Resolve target page.
|
|
689
|
+
let page = 1;
|
|
690
|
+
if (action === GROUPS_ACTION_PAGE) {
|
|
691
|
+
// Page comes from value.page (prev/next button) OR action.option
|
|
692
|
+
// (select_static "jump to page" picker). Same action key, different
|
|
693
|
+
// dispatch field — handler converges on one branch.
|
|
694
|
+
const raw = value.page ?? data.action?.option ?? '1';
|
|
695
|
+
const parsed = Number.parseInt(raw, 10);
|
|
696
|
+
if (Number.isFinite(parsed) && parsed >= 1)
|
|
697
|
+
page = parsed;
|
|
698
|
+
}
|
|
699
|
+
else if (value.page) {
|
|
700
|
+
const parsed = Number.parseInt(value.page, 10);
|
|
701
|
+
if (Number.isFinite(parsed) && parsed >= 1)
|
|
702
|
+
page = parsed;
|
|
703
|
+
}
|
|
704
|
+
const knownActions = new Set([
|
|
705
|
+
GROUPS_ACTION_REFRESH,
|
|
706
|
+
GROUPS_ACTION_PAGE,
|
|
707
|
+
GROUPS_ACTION_DETAIL,
|
|
708
|
+
GROUPS_ACTION_BACK_TO_LIST,
|
|
709
|
+
GROUPS_ACTION_ADD_BOT,
|
|
710
|
+
GROUPS_ACTION_LEAVE_BOT,
|
|
711
|
+
GROUPS_ACTION_ONCALL_BIND,
|
|
712
|
+
GROUPS_ACTION_ONCALL_UNBIND,
|
|
713
|
+
GROUPS_ACTION_ROLE_OPEN,
|
|
714
|
+
GROUPS_ACTION_ROLE_SAVE,
|
|
715
|
+
GROUPS_ACTION_ROLE_DELETE,
|
|
716
|
+
]);
|
|
717
|
+
if (!knownActions.has(String(action))) {
|
|
718
|
+
return ackToast('card.dashboard.settings.invalid_action', locale);
|
|
719
|
+
}
|
|
720
|
+
const client = deps.createClient(larkAppId);
|
|
721
|
+
const load = await loadGroupsMatrix(client, pathSuffix, locale);
|
|
722
|
+
if (!load.ok)
|
|
723
|
+
return load.result;
|
|
724
|
+
const matrix = load.matrix;
|
|
725
|
+
const renderList = (p) => cardResult(buildGroupsCard(matrix, {
|
|
726
|
+
invokerOpenId: operatorOpenId,
|
|
727
|
+
locale,
|
|
728
|
+
page: p,
|
|
729
|
+
pageSize: navPageSize,
|
|
730
|
+
origin: navOrigin,
|
|
731
|
+
scope: navScope,
|
|
732
|
+
}));
|
|
733
|
+
const renderDetail = (chat) => cardResult(buildGroupsDetailCard(matrix, chat, {
|
|
734
|
+
...navOptsBase,
|
|
735
|
+
page,
|
|
736
|
+
}));
|
|
737
|
+
if (action === GROUPS_ACTION_REFRESH || action === GROUPS_ACTION_PAGE || action === GROUPS_ACTION_BACK_TO_LIST) {
|
|
738
|
+
return renderList(page);
|
|
739
|
+
}
|
|
740
|
+
const chat = findChat(matrix, chatId);
|
|
741
|
+
if (!chat) {
|
|
742
|
+
return errorToast('card.dashboard.groups.chat_not_found', undefined, locale);
|
|
743
|
+
}
|
|
744
|
+
if (action === GROUPS_ACTION_DETAIL)
|
|
745
|
+
return renderDetail(chat);
|
|
746
|
+
const member = findDetailMember(matrix, chat, appId);
|
|
747
|
+
if (!member) {
|
|
748
|
+
return errorToast('card.dashboard.groups.bot_not_found', undefined, locale);
|
|
749
|
+
}
|
|
750
|
+
if (action === GROUPS_ACTION_ADD_BOT) {
|
|
751
|
+
if (member.status === 'in') {
|
|
752
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: 'already_in_chat' }, locale);
|
|
753
|
+
}
|
|
754
|
+
const failed = await writeGroupAction(client, 'POST', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/add-bots`, { larkAppIds: [member.larkAppId] }, locale);
|
|
755
|
+
if (failed)
|
|
756
|
+
return failed;
|
|
757
|
+
}
|
|
758
|
+
else if (action === GROUPS_ACTION_LEAVE_BOT) {
|
|
759
|
+
if (member.status !== 'in') {
|
|
760
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: 'not_in_chat' }, locale);
|
|
761
|
+
}
|
|
762
|
+
const failed = await writeGroupAction(client, 'POST', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/leave`, { larkAppIds: [member.larkAppId] }, locale);
|
|
763
|
+
if (failed)
|
|
764
|
+
return failed;
|
|
765
|
+
}
|
|
766
|
+
else if (action === GROUPS_ACTION_ONCALL_BIND) {
|
|
767
|
+
if (!member.bind.enabled) {
|
|
768
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: member.bind.reasonKey ?? 'bind_disabled' }, locale);
|
|
769
|
+
}
|
|
770
|
+
const workingDir = formValue(data, 'working_dir');
|
|
771
|
+
if (!workingDir) {
|
|
772
|
+
return errorToast('card.dashboard.groups.working_dir_required', undefined, locale);
|
|
773
|
+
}
|
|
774
|
+
const failed = await writeGroupAction(client, 'POST', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/oncall/${encodeURIComponent(member.larkAppId)}/bind`, { workingDir }, locale);
|
|
775
|
+
if (failed)
|
|
776
|
+
return failed;
|
|
777
|
+
}
|
|
778
|
+
else if (action === GROUPS_ACTION_ONCALL_UNBIND) {
|
|
779
|
+
if (!member.unbind.enabled) {
|
|
780
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: member.unbind.reasonKey ?? 'unbind_disabled' }, locale);
|
|
781
|
+
}
|
|
782
|
+
const failed = await writeGroupAction(client, 'POST', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/oncall/${encodeURIComponent(member.larkAppId)}/unbind`, undefined, locale);
|
|
783
|
+
if (failed)
|
|
784
|
+
return failed;
|
|
785
|
+
}
|
|
786
|
+
else if (action === GROUPS_ACTION_ROLE_OPEN) {
|
|
787
|
+
if (member.status !== 'in') {
|
|
788
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: 'not_in_chat' }, locale);
|
|
789
|
+
}
|
|
790
|
+
let r;
|
|
791
|
+
try {
|
|
792
|
+
r = await client.request({
|
|
793
|
+
method: 'GET',
|
|
794
|
+
path: `/__daemon/groups/${encodeURIComponent(chat.chatId)}/roles/${encodeURIComponent(member.larkAppId)}`,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
catch (e) {
|
|
798
|
+
return errorToast('card.dashboard.groups.action_failed', { reason: e.message }, locale);
|
|
799
|
+
}
|
|
800
|
+
if (r.status !== 200) {
|
|
801
|
+
return errorToast('card.dashboard.groups.action_failed', { reason: actionFailureReason(r.body, r.status) }, locale);
|
|
802
|
+
}
|
|
803
|
+
const roleContent = typeof r.body?.content === 'string' ? r.body.content : '';
|
|
804
|
+
return cardResult(buildGroupsRoleCard(chat, member, roleContent, { ...navOptsBase, page }));
|
|
805
|
+
}
|
|
806
|
+
else if (action === GROUPS_ACTION_ROLE_SAVE) {
|
|
807
|
+
if (member.status !== 'in') {
|
|
808
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: 'not_in_chat' }, locale);
|
|
809
|
+
}
|
|
810
|
+
const role = formValue(data, 'role');
|
|
811
|
+
if (!role) {
|
|
812
|
+
return errorToast('card.dashboard.groups.role_required', undefined, locale);
|
|
813
|
+
}
|
|
814
|
+
const failed = await writeGroupAction(client, 'PUT', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/roles/${encodeURIComponent(member.larkAppId)}`, { content: role }, locale);
|
|
815
|
+
if (failed)
|
|
816
|
+
return failed;
|
|
817
|
+
}
|
|
818
|
+
else if (action === GROUPS_ACTION_ROLE_DELETE) {
|
|
819
|
+
if (member.status !== 'in') {
|
|
820
|
+
return errorToast('card.dashboard.groups.action_not_allowed', { reason: 'not_in_chat' }, locale);
|
|
821
|
+
}
|
|
822
|
+
const failed = await writeGroupAction(client, 'DELETE', `/__daemon/groups/${encodeURIComponent(chat.chatId)}/roles/${encodeURIComponent(member.larkAppId)}`, undefined, locale);
|
|
823
|
+
if (failed)
|
|
824
|
+
return failed;
|
|
825
|
+
}
|
|
826
|
+
const fresh = await loadGroupsMatrix(client, pathSuffix, locale);
|
|
827
|
+
if (!fresh.ok)
|
|
828
|
+
return fresh.result;
|
|
829
|
+
const freshChat = findChat(fresh.matrix, chat.chatId) ?? chat;
|
|
830
|
+
return cardResult(buildGroupsDetailCard(fresh.matrix, freshChat, { ...navOptsBase, page }));
|
|
831
|
+
}
|
|
832
|
+
//# sourceMappingURL=groups-card.js.map
|