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.
Files changed (178) hide show
  1. package/dist/bot-registry.d.ts +2 -0
  2. package/dist/bot-registry.d.ts.map +1 -1
  3. package/dist/bot-registry.js +4 -0
  4. package/dist/bot-registry.js.map +1 -1
  5. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  6. package/dist/cli/dashboard-endpoint.js +9 -3
  7. package/dist/cli/dashboard-endpoint.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +11 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/core/command-handler.d.ts.map +1 -1
  12. package/dist/core/command-handler.js +9 -1
  13. package/dist/core/command-handler.js.map +1 -1
  14. package/dist/core/dashboard-command/groups.d.ts +20 -0
  15. package/dist/core/dashboard-command/groups.d.ts.map +1 -0
  16. package/dist/core/dashboard-command/groups.js +52 -0
  17. package/dist/core/dashboard-command/groups.js.map +1 -0
  18. package/dist/core/dashboard-command/index.d.ts +30 -0
  19. package/dist/core/dashboard-command/index.d.ts.map +1 -0
  20. package/dist/core/dashboard-command/index.js +81 -0
  21. package/dist/core/dashboard-command/index.js.map +1 -0
  22. package/dist/core/dashboard-command/overview.d.ts +29 -0
  23. package/dist/core/dashboard-command/overview.d.ts.map +1 -0
  24. package/dist/core/dashboard-command/overview.js +61 -0
  25. package/dist/core/dashboard-command/overview.js.map +1 -0
  26. package/dist/core/dashboard-command/owner-gate.d.ts +41 -0
  27. package/dist/core/dashboard-command/owner-gate.d.ts.map +1 -0
  28. package/dist/core/dashboard-command/owner-gate.js +40 -0
  29. package/dist/core/dashboard-command/owner-gate.js.map +1 -0
  30. package/dist/core/dashboard-command/schedules.d.ts +20 -0
  31. package/dist/core/dashboard-command/schedules.d.ts.map +1 -0
  32. package/dist/core/dashboard-command/schedules.js +47 -0
  33. package/dist/core/dashboard-command/schedules.js.map +1 -0
  34. package/dist/core/dashboard-command/sessions.d.ts +22 -0
  35. package/dist/core/dashboard-command/sessions.d.ts.map +1 -0
  36. package/dist/core/dashboard-command/sessions.js +43 -0
  37. package/dist/core/dashboard-command/sessions.js.map +1 -0
  38. package/dist/core/dashboard-command/settings.d.ts +21 -0
  39. package/dist/core/dashboard-command/settings.d.ts.map +1 -0
  40. package/dist/core/dashboard-command/settings.js +55 -0
  41. package/dist/core/dashboard-command/settings.js.map +1 -0
  42. package/dist/core/dashboard-command/stub.d.ts +16 -0
  43. package/dist/core/dashboard-command/stub.d.ts.map +1 -0
  44. package/dist/core/dashboard-command/stub.js +28 -0
  45. package/dist/core/dashboard-command/stub.js.map +1 -0
  46. package/dist/core/dashboard-command/workflows.d.ts +20 -0
  47. package/dist/core/dashboard-command/workflows.d.ts.map +1 -0
  48. package/dist/core/dashboard-command/workflows.js +45 -0
  49. package/dist/core/dashboard-command/workflows.js.map +1 -0
  50. package/dist/core/passthrough-commands.d.ts.map +1 -1
  51. package/dist/core/passthrough-commands.js +1 -1
  52. package/dist/core/passthrough-commands.js.map +1 -1
  53. package/dist/core/session-create.d.ts +0 -1
  54. package/dist/core/session-create.d.ts.map +1 -1
  55. package/dist/core/session-create.js +0 -3
  56. package/dist/core/session-create.js.map +1 -1
  57. package/dist/core/session-manager.d.ts.map +1 -1
  58. package/dist/core/session-manager.js +3 -2
  59. package/dist/core/session-manager.js.map +1 -1
  60. package/dist/daemon-internal-client-wrapper.d.ts +41 -0
  61. package/dist/daemon-internal-client-wrapper.d.ts.map +1 -0
  62. package/dist/daemon-internal-client-wrapper.js +65 -0
  63. package/dist/daemon-internal-client-wrapper.js.map +1 -0
  64. package/dist/dashboard/aggregator.d.ts +12 -0
  65. package/dist/dashboard/aggregator.d.ts.map +1 -1
  66. package/dist/dashboard/aggregator.js +16 -0
  67. package/dist/dashboard/aggregator.js.map +1 -1
  68. package/dist/dashboard/auth.d.ts +7 -0
  69. package/dist/dashboard/auth.d.ts.map +1 -1
  70. package/dist/dashboard/auth.js +21 -0
  71. package/dist/dashboard/auth.js.map +1 -1
  72. package/dist/dashboard/card-model-types.d.ts +50 -0
  73. package/dist/dashboard/card-model-types.d.ts.map +1 -0
  74. package/dist/dashboard/card-model-types.js +9 -0
  75. package/dist/dashboard/card-model-types.js.map +1 -0
  76. package/dist/dashboard/daemon-internal-api.d.ts +90 -0
  77. package/dist/dashboard/daemon-internal-api.d.ts.map +1 -0
  78. package/dist/dashboard/daemon-internal-api.js +534 -0
  79. package/dist/dashboard/daemon-internal-api.js.map +1 -0
  80. package/dist/dashboard/daemon-internal-auth.d.ts +124 -0
  81. package/dist/dashboard/daemon-internal-auth.d.ts.map +1 -0
  82. package/dist/dashboard/daemon-internal-auth.js +188 -0
  83. package/dist/dashboard/daemon-internal-auth.js.map +1 -0
  84. package/dist/dashboard/daemon-internal-client.d.ts +67 -0
  85. package/dist/dashboard/daemon-internal-client.d.ts.map +1 -0
  86. package/dist/dashboard/daemon-internal-client.js +157 -0
  87. package/dist/dashboard/daemon-internal-client.js.map +1 -0
  88. package/dist/dashboard/dashboard-admins.d.ts +8 -0
  89. package/dist/dashboard/dashboard-admins.d.ts.map +1 -0
  90. package/dist/dashboard/dashboard-admins.js +15 -0
  91. package/dist/dashboard/dashboard-admins.js.map +1 -0
  92. package/dist/dashboard/federation-spoke-api.d.ts +3 -0
  93. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  94. package/dist/dashboard/federation-spoke-api.js +11 -8
  95. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  96. package/dist/dashboard/groups-action-helpers.d.ts +69 -0
  97. package/dist/dashboard/groups-action-helpers.d.ts.map +1 -0
  98. package/dist/dashboard/groups-action-helpers.js +141 -0
  99. package/dist/dashboard/groups-action-helpers.js.map +1 -0
  100. package/dist/dashboard/groups-card-model.d.ts +120 -0
  101. package/dist/dashboard/groups-card-model.d.ts.map +1 -0
  102. package/dist/dashboard/groups-card-model.js +175 -0
  103. package/dist/dashboard/groups-card-model.js.map +1 -0
  104. package/dist/dashboard/schedule-card-model.d.ts +172 -0
  105. package/dist/dashboard/schedule-card-model.d.ts.map +1 -0
  106. package/dist/dashboard/schedule-card-model.js +290 -0
  107. package/dist/dashboard/schedule-card-model.js.map +1 -0
  108. package/dist/dashboard/session-card-model.d.ts +83 -0
  109. package/dist/dashboard/session-card-model.d.ts.map +1 -0
  110. package/dist/dashboard/session-card-model.js +196 -0
  111. package/dist/dashboard/session-card-model.js.map +1 -0
  112. package/dist/dashboard/settings-card-model.d.ts +73 -0
  113. package/dist/dashboard/settings-card-model.d.ts.map +1 -0
  114. package/dist/dashboard/settings-card-model.js +94 -0
  115. package/dist/dashboard/settings-card-model.js.map +1 -0
  116. package/dist/dashboard/settings-owner-resolver.d.ts +32 -0
  117. package/dist/dashboard/settings-owner-resolver.d.ts.map +1 -0
  118. package/dist/dashboard/settings-owner-resolver.js +43 -0
  119. package/dist/dashboard/settings-owner-resolver.js.map +1 -0
  120. package/dist/dashboard/settings-write-applier.d.ts +84 -0
  121. package/dist/dashboard/settings-write-applier.d.ts.map +1 -0
  122. package/dist/dashboard/settings-write-applier.js +119 -0
  123. package/dist/dashboard/settings-write-applier.js.map +1 -0
  124. package/dist/dashboard/workflow-api.d.ts.map +1 -1
  125. package/dist/dashboard/workflow-api.js +38 -119
  126. package/dist/dashboard/workflow-api.js.map +1 -1
  127. package/dist/dashboard/workflow-card-model.d.ts +127 -0
  128. package/dist/dashboard/workflow-card-model.d.ts.map +1 -0
  129. package/dist/dashboard/workflow-card-model.js +186 -0
  130. package/dist/dashboard/workflow-card-model.js.map +1 -0
  131. package/dist/dashboard/workflows-action-helpers.d.ts +105 -0
  132. package/dist/dashboard/workflows-action-helpers.d.ts.map +1 -0
  133. package/dist/dashboard/workflows-action-helpers.js +196 -0
  134. package/dist/dashboard/workflows-action-helpers.js.map +1 -0
  135. package/dist/dashboard.js +196 -271
  136. package/dist/dashboard.js.map +1 -1
  137. package/dist/i18n/en.d.ts.map +1 -1
  138. package/dist/i18n/en.js +266 -0
  139. package/dist/i18n/en.js.map +1 -1
  140. package/dist/i18n/zh.d.ts.map +1 -1
  141. package/dist/i18n/zh.js +266 -0
  142. package/dist/i18n/zh.js.map +1 -1
  143. package/dist/im/lark/card-builder.d.ts +2 -0
  144. package/dist/im/lark/card-builder.d.ts.map +1 -1
  145. package/dist/im/lark/card-builder.js +2 -1
  146. package/dist/im/lark/card-builder.js.map +1 -1
  147. package/dist/im/lark/card-handler.d.ts +54 -2
  148. package/dist/im/lark/card-handler.d.ts.map +1 -1
  149. package/dist/im/lark/card-handler.js +127 -1
  150. package/dist/im/lark/card-handler.js.map +1 -1
  151. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  152. package/dist/im/lark/event-dispatcher.js +22 -0
  153. package/dist/im/lark/event-dispatcher.js.map +1 -1
  154. package/dist/im/lark/groups-card.d.ts +90 -0
  155. package/dist/im/lark/groups-card.d.ts.map +1 -0
  156. package/dist/im/lark/groups-card.js +832 -0
  157. package/dist/im/lark/groups-card.js.map +1 -0
  158. package/dist/im/lark/overview-card.d.ts +101 -0
  159. package/dist/im/lark/overview-card.d.ts.map +1 -0
  160. package/dist/im/lark/overview-card.js +475 -0
  161. package/dist/im/lark/overview-card.js.map +1 -0
  162. package/dist/im/lark/schedules-card.d.ts +96 -0
  163. package/dist/im/lark/schedules-card.d.ts.map +1 -0
  164. package/dist/im/lark/schedules-card.js +892 -0
  165. package/dist/im/lark/schedules-card.js.map +1 -0
  166. package/dist/im/lark/sessions-card.d.ts +117 -0
  167. package/dist/im/lark/sessions-card.d.ts.map +1 -0
  168. package/dist/im/lark/sessions-card.js +870 -0
  169. package/dist/im/lark/sessions-card.js.map +1 -0
  170. package/dist/im/lark/settings-card.d.ts +125 -0
  171. package/dist/im/lark/settings-card.d.ts.map +1 -0
  172. package/dist/im/lark/settings-card.js +433 -0
  173. package/dist/im/lark/settings-card.js.map +1 -0
  174. package/dist/im/lark/workflows-card.d.ts +102 -0
  175. package/dist/im/lark/workflows-card.d.ts.map +1 -0
  176. package/dist/im/lark/workflows-card.js +737 -0
  177. package/dist/im/lark/workflows-card.js.map +1 -0
  178. 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
+ * → `&lt;` doesn't get re-encoded as `&amp;lt;`.
555
+ */
556
+ function escapeLarkMd(text) {
557
+ return text
558
+ .replace(/&/g, '&amp;')
559
+ .replace(/</g, '&lt;')
560
+ .replace(/>/g, '&gt;')
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