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,892 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedules dashboard card.
|
|
3
|
+
*
|
|
4
|
+
* The list view is compact, paginated, refreshable, and read-only except for
|
|
5
|
+
* per-row detail entry. The detail view exposes pause/resume, delivery-mode
|
|
6
|
+
* switching, and back actions. Run-now remains outside this card because it is
|
|
7
|
+
* a one-shot side effect rather than a reversible state change.
|
|
8
|
+
*
|
|
9
|
+
* Pause/resume callbacks block on Route B and return a rebuilt card on success.
|
|
10
|
+
* Pause can be rendered from the pre-POST snapshot plus `enabled:false`; resume
|
|
11
|
+
* refetches because scheduler-owned nextRunAt calculation must remain
|
|
12
|
+
* authoritative. Failures return toast-only and leave the card unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Security:
|
|
15
|
+
* - `invokerOpenId` pins callbacks to the admin who opened the card.
|
|
16
|
+
* - sender union_id never lands on `action.value`.
|
|
17
|
+
* - command entry and callbacks both enforce the dashboard admin gate.
|
|
18
|
+
* - row ids are routing keys only; Route B still enforces row ownership.
|
|
19
|
+
* - write actions re-run the model availability matrix before POSTing.
|
|
20
|
+
*/
|
|
21
|
+
import { isDashboardAdmin } from '../../dashboard/dashboard-admins.js';
|
|
22
|
+
import { computeDeliveryButtonAvailability, computeButtonAvailability, normalizeScheduleDelivery, paginateSchedules, toScheduleDetailDto, toScheduleRowDto, } from '../../dashboard/schedule-card-model.js';
|
|
23
|
+
import { t } from '../../i18n/index.js';
|
|
24
|
+
export const SCHEDULES_ACTION_REFRESH = 'dash_schedules_refresh';
|
|
25
|
+
export const SCHEDULES_ACTION_PAGE = 'dash_schedules_page';
|
|
26
|
+
export const SCHEDULES_ACTION_DETAIL = 'dash_schedules_detail';
|
|
27
|
+
export const SCHEDULES_ACTION_PAUSE = 'dash_schedules_pause';
|
|
28
|
+
export const SCHEDULES_ACTION_RESUME = 'dash_schedules_resume';
|
|
29
|
+
export const SCHEDULES_ACTION_DELIVERY = 'dash_schedules_delivery';
|
|
30
|
+
export const SCHEDULES_ACTION_BACK_TO_LIST = 'dash_schedules_back_to_list';
|
|
31
|
+
/** Action emitted by "🔙 返回总览" on overview-origin sub-cards. Same string
|
|
32
|
+
* as overview-card's OVERVIEW_ACTION_REFRESH (kept in sync; we don't import
|
|
33
|
+
* to avoid a circular dep). */
|
|
34
|
+
const BACK_TO_OVERVIEW_ACTION = 'dash_overview_refresh';
|
|
35
|
+
/** Default page size for standalone and overview-drilldown list cards. */
|
|
36
|
+
const PAGE_SIZE = 5;
|
|
37
|
+
/** Hard cap on `select_static` jump-page option count. */
|
|
38
|
+
const JUMP_PAGE_MAX_OPTIONS = 50;
|
|
39
|
+
function toneIcon(tone) {
|
|
40
|
+
switch (tone) {
|
|
41
|
+
case 'success': return '🟢';
|
|
42
|
+
case 'info': return '🔵';
|
|
43
|
+
case 'warning': return '🟡';
|
|
44
|
+
case 'danger': return '🔴';
|
|
45
|
+
case 'neutral': return '⚪';
|
|
46
|
+
default: return '⚫';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function routeBWriteFailureReason(resp) {
|
|
50
|
+
const body = (resp.body ?? {});
|
|
51
|
+
if (resp.status !== 200)
|
|
52
|
+
return String(body.error ?? `http_${resp.status}`);
|
|
53
|
+
if (body.ok === false)
|
|
54
|
+
return String(body.error ?? 'ok_false');
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sort schedules per the Web UI semantics:
|
|
59
|
+
* - enabled first (paused tasks sink),
|
|
60
|
+
* - then by earliest `nextRunAt` (ISO string compare works because ISO
|
|
61
|
+
* strings sort lexicographically same as chronologically).
|
|
62
|
+
* - tasks without `nextRunAt` sort last within their enabled group.
|
|
63
|
+
*/
|
|
64
|
+
function sortForList(tasks) {
|
|
65
|
+
return tasks.slice().sort((a, b) => {
|
|
66
|
+
if (a.enabled !== b.enabled)
|
|
67
|
+
return a.enabled ? -1 : 1;
|
|
68
|
+
const an = a.nextRunAt ?? '';
|
|
69
|
+
const bn = b.nextRunAt ?? '';
|
|
70
|
+
if (an === bn)
|
|
71
|
+
return 0;
|
|
72
|
+
if (!an)
|
|
73
|
+
return 1;
|
|
74
|
+
if (!bn)
|
|
75
|
+
return -1;
|
|
76
|
+
return an < bn ? -1 : 1;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Build the schedules list card JSON. Pure (sorts + paginates + renders). */
|
|
80
|
+
export function buildSchedulesCard(tasks, opts, nowMs) {
|
|
81
|
+
const effectivePageSize = typeof opts.pageSize === 'number' && Number.isFinite(opts.pageSize) && opts.pageSize > 0
|
|
82
|
+
? Math.floor(opts.pageSize)
|
|
83
|
+
: PAGE_SIZE;
|
|
84
|
+
const sorted = sortForList(tasks);
|
|
85
|
+
const { items, total, page, totalPages } = paginateSchedules(sorted, opts.page, effectivePageSize);
|
|
86
|
+
// Plumb origin + page_size + scope onto every button.value so refresh/
|
|
87
|
+
// page/detail/detail-back rebuilds keep the same drilldown state.
|
|
88
|
+
const navFields = {};
|
|
89
|
+
if (opts.origin === 'overview')
|
|
90
|
+
navFields.origin = 'overview';
|
|
91
|
+
if (effectivePageSize !== PAGE_SIZE)
|
|
92
|
+
navFields.page_size = String(effectivePageSize);
|
|
93
|
+
if (opts.scope === 'global')
|
|
94
|
+
navFields.dashboard_scope = 'global';
|
|
95
|
+
const enabledCount = sorted.filter(t => t.enabled).length;
|
|
96
|
+
const pausedCount = total - enabledCount;
|
|
97
|
+
const elements = [];
|
|
98
|
+
elements.push({
|
|
99
|
+
tag: 'div',
|
|
100
|
+
text: {
|
|
101
|
+
tag: 'lark_md',
|
|
102
|
+
content: t('card.dashboard.schedules.count_summary', {
|
|
103
|
+
enabled: String(enabledCount),
|
|
104
|
+
paused: String(pausedCount),
|
|
105
|
+
page: String(page),
|
|
106
|
+
totalPages: String(totalPages),
|
|
107
|
+
}, opts.locale),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
elements.push({ tag: 'hr' });
|
|
111
|
+
if (items.length === 0) {
|
|
112
|
+
elements.push({
|
|
113
|
+
tag: 'div',
|
|
114
|
+
text: {
|
|
115
|
+
tag: 'lark_md',
|
|
116
|
+
content: t('card.dashboard.schedules.empty', undefined, opts.locale),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const task of items) {
|
|
122
|
+
const dto = toScheduleRowDto(task, { nowMs });
|
|
123
|
+
// Row text element.
|
|
124
|
+
elements.push(renderRow(dto, opts.locale, opts.scope === 'global'));
|
|
125
|
+
// Per-row action element holding ONLY the "📂 详情" button.
|
|
126
|
+
// Keeping each row's actions in its own `action` element (rather than
|
|
127
|
+
// one shared element for the whole page) makes the visual layout
|
|
128
|
+
// align with the row above and lets us pass the row's schedule id
|
|
129
|
+
// through `value.schedule_id` as the routing key.
|
|
130
|
+
elements.push({
|
|
131
|
+
tag: 'action',
|
|
132
|
+
actions: [
|
|
133
|
+
{
|
|
134
|
+
tag: 'button',
|
|
135
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.row_detail', undefined, opts.locale) },
|
|
136
|
+
type: 'default',
|
|
137
|
+
value: {
|
|
138
|
+
action: SCHEDULES_ACTION_DETAIL,
|
|
139
|
+
invoker_open_id: opts.invokerOpenId,
|
|
140
|
+
schedule_id: dto.id,
|
|
141
|
+
page: String(page),
|
|
142
|
+
...navFields,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
elements.push({ tag: 'hr' });
|
|
150
|
+
// Pagination + refresh.
|
|
151
|
+
const actions = [];
|
|
152
|
+
if (totalPages > 1) {
|
|
153
|
+
actions.push({
|
|
154
|
+
tag: 'button',
|
|
155
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.prev', undefined, opts.locale) },
|
|
156
|
+
type: 'default',
|
|
157
|
+
disabled: page <= 1,
|
|
158
|
+
value: {
|
|
159
|
+
action: SCHEDULES_ACTION_PAGE,
|
|
160
|
+
invoker_open_id: opts.invokerOpenId,
|
|
161
|
+
page: String(Math.max(1, page - 1)),
|
|
162
|
+
...navFields,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
actions.push({
|
|
166
|
+
tag: 'button',
|
|
167
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.next', undefined, opts.locale) },
|
|
168
|
+
type: 'default',
|
|
169
|
+
disabled: page >= totalPages,
|
|
170
|
+
value: {
|
|
171
|
+
action: SCHEDULES_ACTION_PAGE,
|
|
172
|
+
invoker_open_id: opts.invokerOpenId,
|
|
173
|
+
page: String(Math.min(totalPages, page + 1)),
|
|
174
|
+
...navFields,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
// "Jump to page" select — same action as prev/next, page comes via
|
|
178
|
+
// action.option. Handler reads `value.page ?? action.option ?? '1'`.
|
|
179
|
+
// Capped at JUMP_PAGE_MAX_OPTIONS to keep payload small.
|
|
180
|
+
if (totalPages > 2 && totalPages <= JUMP_PAGE_MAX_OPTIONS) {
|
|
181
|
+
const options = Array.from({ length: totalPages }, (_, i) => {
|
|
182
|
+
const n = i + 1;
|
|
183
|
+
return {
|
|
184
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.jump_page', { n: String(n), total: String(totalPages) }, opts.locale) },
|
|
185
|
+
value: String(n),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
actions.push({
|
|
189
|
+
tag: 'select_static',
|
|
190
|
+
placeholder: {
|
|
191
|
+
tag: 'plain_text',
|
|
192
|
+
content: t('card.dashboard.schedules.jump_page', { n: String(page), total: String(totalPages) }, opts.locale),
|
|
193
|
+
},
|
|
194
|
+
initial_option: String(page),
|
|
195
|
+
options,
|
|
196
|
+
value: {
|
|
197
|
+
action: SCHEDULES_ACTION_PAGE,
|
|
198
|
+
invoker_open_id: opts.invokerOpenId,
|
|
199
|
+
...navFields,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
actions.push({
|
|
205
|
+
tag: 'button',
|
|
206
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.refresh', undefined, opts.locale) },
|
|
207
|
+
type: 'default',
|
|
208
|
+
value: {
|
|
209
|
+
action: SCHEDULES_ACTION_REFRESH,
|
|
210
|
+
invoker_open_id: opts.invokerOpenId,
|
|
211
|
+
...navFields,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
// Overview drilldown only — back-to-overview reuses overview-refresh
|
|
215
|
+
// action; card-handler routes by action prefix.
|
|
216
|
+
if (opts.origin === 'overview') {
|
|
217
|
+
actions.push({
|
|
218
|
+
tag: 'button',
|
|
219
|
+
text: { tag: 'plain_text', content: t('card.dashboard.overview.back_button', undefined, opts.locale) },
|
|
220
|
+
type: 'default',
|
|
221
|
+
value: {
|
|
222
|
+
action: BACK_TO_OVERVIEW_ACTION,
|
|
223
|
+
invoker_open_id: opts.invokerOpenId,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
elements.push({ tag: 'action', actions });
|
|
228
|
+
elements.push({
|
|
229
|
+
tag: 'note',
|
|
230
|
+
elements: [
|
|
231
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
return JSON.stringify({
|
|
235
|
+
config: { wide_screen_mode: true },
|
|
236
|
+
header: {
|
|
237
|
+
title: { tag: 'plain_text', content: t('card.dashboard.schedules.title', undefined, opts.locale) },
|
|
238
|
+
template: 'blue',
|
|
239
|
+
},
|
|
240
|
+
elements,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function renderRow(row, locale, showBotLabel) {
|
|
244
|
+
const icon = toneIcon(row.dot.tone);
|
|
245
|
+
const errorGlyph = row.errorIndicator ? ' ⚠️' : '';
|
|
246
|
+
// Primary: status icon + bold name + short id (8 chars) for traceability —
|
|
247
|
+
// schedules can share names (e.g. two `daily-ping` cron tasks in different
|
|
248
|
+
// chats), so we surface a stable disambiguator.
|
|
249
|
+
// Secondary: kind · displayExpr · next · last (· repeat counter if finite)
|
|
250
|
+
const shortId = row.id ? row.id.slice(0, 8) : '';
|
|
251
|
+
const repeatStr = row.repeat
|
|
252
|
+
? row.repeat.times === null
|
|
253
|
+
? `${row.repeat.completed}/∞`
|
|
254
|
+
: `${row.repeat.completed}/${row.repeat.times}`
|
|
255
|
+
: undefined;
|
|
256
|
+
const secondaryParts = [
|
|
257
|
+
row.kind,
|
|
258
|
+
row.displayExpr,
|
|
259
|
+
t('card.dashboard.schedules.next_label', { rel: row.nextRunRelative }, locale),
|
|
260
|
+
t('card.dashboard.schedules.last_label', { rel: row.lastRunRelative }, locale),
|
|
261
|
+
];
|
|
262
|
+
if (repeatStr) {
|
|
263
|
+
secondaryParts.push(t('card.dashboard.schedules.repeat_label', { repeat: repeatStr }, locale));
|
|
264
|
+
}
|
|
265
|
+
// In global scope, prefix rows with the owning bot so same-name schedules
|
|
266
|
+
// remain distinguishable.
|
|
267
|
+
// Preference: row.raw.botName → larkAppId short-suffix → '—'.
|
|
268
|
+
if (showBotLabel) {
|
|
269
|
+
const botLabel = botLabelFromRow(row);
|
|
270
|
+
secondaryParts.unshift(t('card.dashboard.schedules.bot_label', { bot: botLabel }, locale));
|
|
271
|
+
}
|
|
272
|
+
const idSuffix = shortId ? ` <font color="grey">${escapeLarkMd(shortId)}</font>` : '';
|
|
273
|
+
return {
|
|
274
|
+
tag: 'div',
|
|
275
|
+
text: {
|
|
276
|
+
tag: 'lark_md',
|
|
277
|
+
content: `${icon} **${escapeLarkMd(row.name)}**${idSuffix}${errorGlyph}` +
|
|
278
|
+
`\n<font color="grey">${escapeLarkMd(secondaryParts.join(' · '))}</font>`,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/** Resolve the human label for a schedule's owning bot. */
|
|
283
|
+
function botLabelFromRow(row) {
|
|
284
|
+
const raw = row.raw;
|
|
285
|
+
if (typeof raw.botName === 'string' && raw.botName.length > 0)
|
|
286
|
+
return raw.botName;
|
|
287
|
+
if (typeof raw.larkAppId === 'string' && raw.larkAppId.length > 0) {
|
|
288
|
+
return `bot:${raw.larkAppId.slice(-6)}`;
|
|
289
|
+
}
|
|
290
|
+
return '—';
|
|
291
|
+
}
|
|
292
|
+
/** Resolve the owning bot label for a schedule detail row. */
|
|
293
|
+
function botLabelFromDetail(detail) {
|
|
294
|
+
const raw = detail.raw;
|
|
295
|
+
if (typeof raw.botName === 'string' && raw.botName.length > 0)
|
|
296
|
+
return raw.botName;
|
|
297
|
+
if (typeof detail.larkAppId === 'string' && detail.larkAppId.length > 0) {
|
|
298
|
+
return `bot:${detail.larkAppId.slice(-6)}`;
|
|
299
|
+
}
|
|
300
|
+
if (typeof raw.larkAppId === 'string' && raw.larkAppId.length > 0) {
|
|
301
|
+
return `bot:${raw.larkAppId.slice(-6)}`;
|
|
302
|
+
}
|
|
303
|
+
return '—';
|
|
304
|
+
}
|
|
305
|
+
function deliveryLabel(deliver, locale) {
|
|
306
|
+
switch (deliver) {
|
|
307
|
+
case 'new-topic':
|
|
308
|
+
return t('card.dashboard.schedules.delivery.new_topic', undefined, locale);
|
|
309
|
+
case 'local':
|
|
310
|
+
return t('card.dashboard.schedules.delivery.local', undefined, locale);
|
|
311
|
+
case 'origin':
|
|
312
|
+
default:
|
|
313
|
+
return t('card.dashboard.schedules.delivery.origin', undefined, locale);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function nextDeliveryTarget(deliver) {
|
|
317
|
+
return deliver === 'new-topic' ? 'origin' : 'new-topic';
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build the schedule detail card: task metadata, next-run preview, reversible
|
|
321
|
+
* state controls, delivery-mode switch, and back action.
|
|
322
|
+
*
|
|
323
|
+
* Pause and resume are mutually exclusive per the model matrix:
|
|
324
|
+
* enabled === true → pause clickable, resume disabled (alreadyEnabled)
|
|
325
|
+
* enabled === false → pause disabled (alreadyPaused), resume clickable
|
|
326
|
+
*
|
|
327
|
+
* The disabled button is rendered with `disabled:true` and a small reason
|
|
328
|
+
* note next to it via `mapPauseDisabledReason` / `mapResumeDisabledReason`.
|
|
329
|
+
*/
|
|
330
|
+
export function buildSchedulesDetailCard(detail, opts) {
|
|
331
|
+
const elements = [];
|
|
332
|
+
// ─── Title — status dot + bold name + id monospace ─────────────────────
|
|
333
|
+
const enabledIcon = detail.enabled
|
|
334
|
+
? (detail.errorIndicator ? '🔴' : '🟢')
|
|
335
|
+
: '⚪';
|
|
336
|
+
elements.push({
|
|
337
|
+
tag: 'div',
|
|
338
|
+
text: {
|
|
339
|
+
tag: 'lark_md',
|
|
340
|
+
content: `${enabledIcon} **${escapeLarkMd(detail.name)}**` +
|
|
341
|
+
`\n\`${escapeLarkMd(detail.id)}\``,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
elements.push({ tag: 'hr' });
|
|
345
|
+
// ─── Key/value block ───────────────────────────────────────────────────
|
|
346
|
+
const enabledLabel = detail.enabled
|
|
347
|
+
? t('card.dashboard.schedules.detail.enabled.active', undefined, opts.locale)
|
|
348
|
+
: t('card.dashboard.schedules.detail.enabled.paused', undefined, opts.locale);
|
|
349
|
+
const infoLines = [];
|
|
350
|
+
infoLines.push(t('card.dashboard.schedules.detail.name_label', { name: escapeLarkMd(detail.name) }, opts.locale));
|
|
351
|
+
infoLines.push(t('card.dashboard.schedules.detail.enabled_label', { status: enabledLabel }, opts.locale));
|
|
352
|
+
infoLines.push(t('card.dashboard.schedules.detail.kind_label', { kind: escapeLarkMd(detail.kind) }, opts.locale));
|
|
353
|
+
infoLines.push(t('card.dashboard.schedules.detail.display_label', { expr: escapeLarkMd(detail.displayExpr) }, opts.locale));
|
|
354
|
+
infoLines.push(t('card.dashboard.schedules.detail.delivery_label', { delivery: escapeLarkMd(deliveryLabel(detail.deliver, opts.locale)) }, opts.locale));
|
|
355
|
+
if (opts.scope === 'global') {
|
|
356
|
+
infoLines.push(t('card.dashboard.schedules.detail.owner_label', {
|
|
357
|
+
bot: escapeLarkMd(botLabelFromDetail(detail)),
|
|
358
|
+
chat: escapeLarkMd(detail.chatId ?? '—'),
|
|
359
|
+
}, opts.locale));
|
|
360
|
+
}
|
|
361
|
+
infoLines.push(t('card.dashboard.schedules.detail.next_label', { time: escapeLarkMd(detail.nextRunAt ?? '—') }, opts.locale));
|
|
362
|
+
infoLines.push(t('card.dashboard.schedules.detail.last_label', { time: escapeLarkMd(detail.lastRunAt ?? '—') }, opts.locale));
|
|
363
|
+
if (detail.lastStatus) {
|
|
364
|
+
infoLines.push(t('card.dashboard.schedules.detail.status_label', { status: escapeLarkMd(detail.lastStatus) }, opts.locale));
|
|
365
|
+
}
|
|
366
|
+
if (detail.repeat) {
|
|
367
|
+
const repeatStr = detail.repeat.times === null
|
|
368
|
+
? `${detail.repeat.completed}/∞`
|
|
369
|
+
: `${detail.repeat.completed}/${detail.repeat.times}`;
|
|
370
|
+
infoLines.push(t('card.dashboard.schedules.detail.repeat_label', { repeat: repeatStr }, opts.locale));
|
|
371
|
+
}
|
|
372
|
+
if (detail.prompt) {
|
|
373
|
+
// `detail.prompt` is already truncated by `toScheduleDetailDto`. When the
|
|
374
|
+
// raw prompt was longer than `promptTruncateAt` the DTO appends `…`, so
|
|
375
|
+
// we can render it verbatim and rely on that visible marker.
|
|
376
|
+
infoLines.push(t('card.dashboard.schedules.detail.prompt_label', { prompt: escapeLarkMd(detail.prompt) }, opts.locale));
|
|
377
|
+
}
|
|
378
|
+
elements.push({
|
|
379
|
+
tag: 'div',
|
|
380
|
+
text: {
|
|
381
|
+
tag: 'lark_md',
|
|
382
|
+
content: infoLines.map(l => `<font color="grey">${l}</font>`).join('\n'),
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
// ─── Next runs section (if any) ────────────────────────────────────────
|
|
386
|
+
if (detail.nextRuns.length > 0) {
|
|
387
|
+
elements.push({ tag: 'hr' });
|
|
388
|
+
elements.push({
|
|
389
|
+
tag: 'div',
|
|
390
|
+
text: {
|
|
391
|
+
tag: 'lark_md',
|
|
392
|
+
content: `**${t('card.dashboard.schedules.detail.next_runs_header', undefined, opts.locale)}**\n` +
|
|
393
|
+
detail.nextRuns.map(iso => `- ${escapeLarkMd(iso)}`).join('\n'),
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
elements.push({ tag: 'hr' });
|
|
398
|
+
// Threaded nav state — pause/resume rebuild this same detail card on
|
|
399
|
+
// success, so users may then press 🔙 返回 and expect the drilldown list.
|
|
400
|
+
const navFields = {};
|
|
401
|
+
if (opts.origin === 'overview')
|
|
402
|
+
navFields.origin = 'overview';
|
|
403
|
+
if (typeof opts.sourcePage === 'number' && Number.isFinite(opts.sourcePage) && opts.sourcePage >= 1) {
|
|
404
|
+
navFields.page = String(Math.floor(opts.sourcePage));
|
|
405
|
+
}
|
|
406
|
+
if (typeof opts.pageSize === 'number'
|
|
407
|
+
&& Number.isFinite(opts.pageSize)
|
|
408
|
+
&& opts.pageSize > 0
|
|
409
|
+
&& opts.pageSize !== PAGE_SIZE) {
|
|
410
|
+
navFields.page_size = String(Math.floor(opts.pageSize));
|
|
411
|
+
}
|
|
412
|
+
if (opts.scope === 'global')
|
|
413
|
+
navFields.dashboard_scope = 'global';
|
|
414
|
+
// ─── Action row — pause / resume + delivery (mutually exclusive) + back ─
|
|
415
|
+
const pauseEnabled = detail.actions.pause.enabled === true;
|
|
416
|
+
const resumeEnabled = detail.actions.resume.enabled === true;
|
|
417
|
+
const currentDelivery = normalizeScheduleDelivery(detail.raw.deliver);
|
|
418
|
+
const deliveryTarget = nextDeliveryTarget(currentDelivery);
|
|
419
|
+
const deliveryButtonState = computeDeliveryButtonAvailability(detail.raw, deliveryTarget);
|
|
420
|
+
const deliveryEnabled = deliveryButtonState.enabled === true;
|
|
421
|
+
const pauseButton = {
|
|
422
|
+
tag: 'button',
|
|
423
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.btn.pause', undefined, opts.locale) },
|
|
424
|
+
type: pauseEnabled ? 'primary' : 'default',
|
|
425
|
+
value: {
|
|
426
|
+
action: SCHEDULES_ACTION_PAUSE,
|
|
427
|
+
invoker_open_id: opts.invokerOpenId,
|
|
428
|
+
schedule_id: detail.id,
|
|
429
|
+
...navFields,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
if (!pauseEnabled)
|
|
433
|
+
pauseButton.disabled = true;
|
|
434
|
+
const resumeButton = {
|
|
435
|
+
tag: 'button',
|
|
436
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.btn.resume', undefined, opts.locale) },
|
|
437
|
+
type: resumeEnabled ? 'primary' : 'default',
|
|
438
|
+
value: {
|
|
439
|
+
action: SCHEDULES_ACTION_RESUME,
|
|
440
|
+
invoker_open_id: opts.invokerOpenId,
|
|
441
|
+
schedule_id: detail.id,
|
|
442
|
+
...navFields,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
if (!resumeEnabled)
|
|
446
|
+
resumeButton.disabled = true;
|
|
447
|
+
const deliveryButton = {
|
|
448
|
+
tag: 'button',
|
|
449
|
+
text: {
|
|
450
|
+
tag: 'plain_text',
|
|
451
|
+
content: t(deliveryTarget === 'new-topic'
|
|
452
|
+
? 'card.dashboard.schedules.btn.use_new_topic'
|
|
453
|
+
: 'card.dashboard.schedules.btn.use_origin', undefined, opts.locale),
|
|
454
|
+
},
|
|
455
|
+
type: deliveryEnabled ? 'primary' : 'default',
|
|
456
|
+
value: {
|
|
457
|
+
action: SCHEDULES_ACTION_DELIVERY,
|
|
458
|
+
invoker_open_id: opts.invokerOpenId,
|
|
459
|
+
schedule_id: detail.id,
|
|
460
|
+
target_delivery: deliveryTarget,
|
|
461
|
+
...navFields,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
if (!deliveryEnabled)
|
|
465
|
+
deliveryButton.disabled = true;
|
|
466
|
+
elements.push({
|
|
467
|
+
tag: 'action',
|
|
468
|
+
actions: [
|
|
469
|
+
pauseButton,
|
|
470
|
+
resumeButton,
|
|
471
|
+
deliveryButton,
|
|
472
|
+
{
|
|
473
|
+
tag: 'button',
|
|
474
|
+
text: { tag: 'plain_text', content: t('card.dashboard.schedules.btn.back', undefined, opts.locale) },
|
|
475
|
+
type: 'default',
|
|
476
|
+
value: {
|
|
477
|
+
action: SCHEDULES_ACTION_BACK_TO_LIST,
|
|
478
|
+
invoker_open_id: opts.invokerOpenId,
|
|
479
|
+
...navFields,
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
});
|
|
484
|
+
// Surface reasonKey notes for whichever button is disabled. Pause/resume
|
|
485
|
+
// are mutually exclusive today, but render both branches defensively in case
|
|
486
|
+
// the matrix evolves.
|
|
487
|
+
if (!pauseEnabled) {
|
|
488
|
+
const reasonKey = mapPauseDisabledReason(detail.actions.pause.reasonKey);
|
|
489
|
+
if (reasonKey) {
|
|
490
|
+
elements.push({
|
|
491
|
+
tag: 'note',
|
|
492
|
+
elements: [
|
|
493
|
+
{ tag: 'lark_md', content: t(reasonKey, undefined, opts.locale) },
|
|
494
|
+
],
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!resumeEnabled) {
|
|
499
|
+
const reasonKey = mapResumeDisabledReason(detail.actions.resume.reasonKey);
|
|
500
|
+
if (reasonKey) {
|
|
501
|
+
elements.push({
|
|
502
|
+
tag: 'note',
|
|
503
|
+
elements: [
|
|
504
|
+
{ tag: 'lark_md', content: t(reasonKey, undefined, opts.locale) },
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!deliveryEnabled) {
|
|
510
|
+
const reasonKey = mapDeliveryDisabledReason(deliveryButtonState.reasonKey);
|
|
511
|
+
if (reasonKey) {
|
|
512
|
+
elements.push({
|
|
513
|
+
tag: 'note',
|
|
514
|
+
elements: [
|
|
515
|
+
{ tag: 'lark_md', content: t(reasonKey, undefined, opts.locale) },
|
|
516
|
+
],
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Footer security note (mirrors list card).
|
|
521
|
+
elements.push({
|
|
522
|
+
tag: 'note',
|
|
523
|
+
elements: [
|
|
524
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
return JSON.stringify({
|
|
528
|
+
config: { wide_screen_mode: true },
|
|
529
|
+
header: {
|
|
530
|
+
title: { tag: 'plain_text', content: t('card.dashboard.schedules.detail.title', undefined, opts.locale) },
|
|
531
|
+
template: 'blue',
|
|
532
|
+
},
|
|
533
|
+
elements,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/** Map pause-disabled reason keys to card i18n keys. */
|
|
537
|
+
function mapPauseDisabledReason(reasonKey) {
|
|
538
|
+
switch (reasonKey) {
|
|
539
|
+
case 'schedules.action.pause.alreadyPaused':
|
|
540
|
+
return 'card.dashboard.schedules.pause.disabled.alreadyPaused';
|
|
541
|
+
default:
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/** Map resume-disabled reason keys to card i18n keys. */
|
|
546
|
+
function mapResumeDisabledReason(reasonKey) {
|
|
547
|
+
switch (reasonKey) {
|
|
548
|
+
case 'schedules.action.resume.alreadyEnabled':
|
|
549
|
+
return 'card.dashboard.schedules.resume.disabled.alreadyEnabled';
|
|
550
|
+
default:
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function mapDeliveryDisabledReason(reasonKey) {
|
|
555
|
+
switch (reasonKey) {
|
|
556
|
+
case 'schedules.action.delivery.local':
|
|
557
|
+
return 'card.dashboard.schedules.delivery.disabled.local';
|
|
558
|
+
case 'schedules.action.delivery.alreadyOrigin':
|
|
559
|
+
return 'card.dashboard.schedules.delivery.disabled.alreadyOrigin';
|
|
560
|
+
case 'schedules.action.delivery.alreadyNewTopic':
|
|
561
|
+
return 'card.dashboard.schedules.delivery.disabled.alreadyNewTopic';
|
|
562
|
+
default:
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Sanitize user-/CLI-supplied text for inclusion in lark_md. See
|
|
568
|
+
* `sessions-card.ts:escapeLarkMd` for the order rationale: `&` first to
|
|
569
|
+
* avoid `&` → `&` then `<` → `<` getting re-encoded as `&lt;`.
|
|
570
|
+
*/
|
|
571
|
+
function escapeLarkMd(text) {
|
|
572
|
+
return text
|
|
573
|
+
.replace(/&/g, '&')
|
|
574
|
+
.replace(/</g, '<')
|
|
575
|
+
.replace(/>/g, '>')
|
|
576
|
+
.replace(/([*_~`])/g, '\\$1');
|
|
577
|
+
}
|
|
578
|
+
function ackToast(textKey, locale) {
|
|
579
|
+
return { toast: { type: 'info', content: t(textKey, undefined, locale) } };
|
|
580
|
+
}
|
|
581
|
+
function errorToast(textKey, params, locale) {
|
|
582
|
+
return { toast: { type: 'error', content: t(textKey, params, locale) } };
|
|
583
|
+
}
|
|
584
|
+
/** Dispatch a `dash_schedules_*` action callback. Mirrors sessions-card. */
|
|
585
|
+
export async function handleSchedulesCardAction(data, larkAppId, deps) {
|
|
586
|
+
const locale = deps.locale ?? 'zh';
|
|
587
|
+
const value = (data.action?.value ?? {});
|
|
588
|
+
const operatorOpenId = data.operator?.open_id;
|
|
589
|
+
const action = value.action;
|
|
590
|
+
// ─── 1) Invoker lock — fail-closed ──────────────────────────────────
|
|
591
|
+
const invokerOpenId = value.invoker_open_id;
|
|
592
|
+
if (typeof invokerOpenId !== 'string' || !invokerOpenId) {
|
|
593
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
594
|
+
}
|
|
595
|
+
if (typeof operatorOpenId !== 'string' || !operatorOpenId) {
|
|
596
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
597
|
+
}
|
|
598
|
+
if (invokerOpenId !== operatorOpenId) {
|
|
599
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
600
|
+
}
|
|
601
|
+
// ─── 2) Per-bot admin gate ──────────────────────────────────────────
|
|
602
|
+
if (!isDashboardAdmin(larkAppId, operatorOpenId, deps)) {
|
|
603
|
+
return ackToast('card.dashboard.settings.owner_only', locale);
|
|
604
|
+
}
|
|
605
|
+
// Validate the action BEFORE creating the Route B client — an unknown
|
|
606
|
+
// action shouldn't even open a connection.
|
|
607
|
+
const validActions = new Set([
|
|
608
|
+
SCHEDULES_ACTION_REFRESH,
|
|
609
|
+
SCHEDULES_ACTION_PAGE,
|
|
610
|
+
SCHEDULES_ACTION_DETAIL,
|
|
611
|
+
SCHEDULES_ACTION_PAUSE,
|
|
612
|
+
SCHEDULES_ACTION_RESUME,
|
|
613
|
+
SCHEDULES_ACTION_DELIVERY,
|
|
614
|
+
SCHEDULES_ACTION_BACK_TO_LIST,
|
|
615
|
+
]);
|
|
616
|
+
if (!validActions.has(action)) {
|
|
617
|
+
return ackToast('card.dashboard.settings.invalid_action', locale);
|
|
618
|
+
}
|
|
619
|
+
const client = deps.createClient(larkAppId);
|
|
620
|
+
const now = () => (deps.nowMs ? deps.nowMs() : Date.now());
|
|
621
|
+
// ─── Nav state (overview drilldown + global scope) ──────────────────
|
|
622
|
+
// Threaded by buildSchedulesCard onto every button.value; we parse here
|
|
623
|
+
// so the rebuild path keeps the same shape (5/page + 🔙 返回总览 +
|
|
624
|
+
// global scope).
|
|
625
|
+
const navOrigin = value.origin === 'overview' ? 'overview' : undefined;
|
|
626
|
+
const parsedPageSize = Number.parseInt(value.page_size ?? '', 10);
|
|
627
|
+
const navPageSize = Number.isFinite(parsedPageSize) && parsedPageSize > 0 ? parsedPageSize : undefined;
|
|
628
|
+
const parsedNavPage = Number.parseInt(value.page ?? '', 10);
|
|
629
|
+
const navPage = Number.isFinite(parsedNavPage) && parsedNavPage >= 1 ? parsedNavPage : undefined;
|
|
630
|
+
const navScope = value.dashboard_scope === 'global' ? 'global' : undefined;
|
|
631
|
+
const listPathSuffix = navScope === 'global' ? '?scope=global' : '';
|
|
632
|
+
const writePathSuffix = navScope === 'global' ? '?scope=global' : '';
|
|
633
|
+
// ─── 3a) DETAIL — open the per-schedule detail card ─────────────────
|
|
634
|
+
if (action === SCHEDULES_ACTION_DETAIL) {
|
|
635
|
+
const scheduleId = value.schedule_id;
|
|
636
|
+
if (typeof scheduleId !== 'string' || !scheduleId) {
|
|
637
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
638
|
+
}
|
|
639
|
+
const r = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
640
|
+
if ('errorResult' in r)
|
|
641
|
+
return r.errorResult;
|
|
642
|
+
const row = r.tasks.find(t => t.id === scheduleId);
|
|
643
|
+
if (!row) {
|
|
644
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
645
|
+
}
|
|
646
|
+
const detail = toScheduleDetailDto(row, { nowMs: now() });
|
|
647
|
+
const cardJson = buildSchedulesDetailCard(detail, {
|
|
648
|
+
invokerOpenId: operatorOpenId,
|
|
649
|
+
locale,
|
|
650
|
+
origin: navOrigin,
|
|
651
|
+
pageSize: navPageSize,
|
|
652
|
+
sourcePage: navPage,
|
|
653
|
+
scope: navScope,
|
|
654
|
+
});
|
|
655
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
656
|
+
}
|
|
657
|
+
// ─── 3b) PAUSE / RESUME — synchronous toggle ────────────────────────
|
|
658
|
+
if (action === SCHEDULES_ACTION_PAUSE || action === SCHEDULES_ACTION_RESUME) {
|
|
659
|
+
const scheduleId = value.schedule_id;
|
|
660
|
+
if (typeof scheduleId !== 'string' || !scheduleId) {
|
|
661
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
662
|
+
}
|
|
663
|
+
const verb = action === SCHEDULES_ACTION_PAUSE ? 'pause' : 'resume';
|
|
664
|
+
const failedKey = verb === 'pause'
|
|
665
|
+
? 'card.dashboard.schedules.pause_failed'
|
|
666
|
+
: 'card.dashboard.schedules.resume_failed';
|
|
667
|
+
// Pre-POST snapshot — needed both to confirm the schedule still exists
|
|
668
|
+
// AND to synthesize the new-state row in-process so we don't pay the
|
|
669
|
+
// cost (or race) of a second GET on the happy pause path.
|
|
670
|
+
const pre = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
671
|
+
if ('errorResult' in pre)
|
|
672
|
+
return pre.errorResult;
|
|
673
|
+
const before = pre.tasks.find(t => t.id === scheduleId);
|
|
674
|
+
if (!before) {
|
|
675
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
676
|
+
}
|
|
677
|
+
// Client-side `disabled` is UX only; replayed or crafted callbacks can
|
|
678
|
+
// still arrive. Re-run the same availability matrix server-side and
|
|
679
|
+
// fail closed before POSTing.
|
|
680
|
+
const beforeMatrix = computeButtonAvailability(before);
|
|
681
|
+
const buttonState = verb === 'pause' ? beforeMatrix.pause : beforeMatrix.resume;
|
|
682
|
+
if (buttonState.enabled !== true) {
|
|
683
|
+
// Reuse the same reasonKey → i18n mapping the builder uses
|
|
684
|
+
// for the inline disabled-button note so toast text matches what
|
|
685
|
+
// the user already sees on the card. NEVER POST; NEVER redraw.
|
|
686
|
+
const mappedKey = verb === 'pause'
|
|
687
|
+
? mapPauseDisabledReason(buttonState.reasonKey) ?? failedKey
|
|
688
|
+
: mapResumeDisabledReason(buttonState.reasonKey) ?? failedKey;
|
|
689
|
+
return errorToast(mappedKey, undefined, locale);
|
|
690
|
+
}
|
|
691
|
+
// Route B owner routing is the authority on whether this schedule can be
|
|
692
|
+
// toggled; the IM layer only sanitizes the routing key and state matrix.
|
|
693
|
+
let resp;
|
|
694
|
+
try {
|
|
695
|
+
resp = await client.request({
|
|
696
|
+
method: 'POST',
|
|
697
|
+
path: `/__daemon/schedules/${encodeURIComponent(scheduleId)}/${verb}${writePathSuffix}`,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
return errorToast(failedKey, { reason: e.message }, locale);
|
|
702
|
+
}
|
|
703
|
+
const failureReason = routeBWriteFailureReason(resp);
|
|
704
|
+
if (failureReason) {
|
|
705
|
+
// Preserve user state — do NOT redraw card on failure.
|
|
706
|
+
return errorToast(failedKey, { reason: failureReason }, locale);
|
|
707
|
+
}
|
|
708
|
+
if (verb === 'pause') {
|
|
709
|
+
// Pause makes nextRunAt irrelevant — the scheduler stops emitting
|
|
710
|
+
// until the task is resumed — so an in-process overlay is safe.
|
|
711
|
+
const synth = { ...before, enabled: false };
|
|
712
|
+
const detail = toScheduleDetailDto(synth, { nowMs: now() });
|
|
713
|
+
const cardJson = buildSchedulesDetailCard(detail, {
|
|
714
|
+
invokerOpenId: operatorOpenId,
|
|
715
|
+
locale,
|
|
716
|
+
origin: navOrigin,
|
|
717
|
+
pageSize: navPageSize,
|
|
718
|
+
sourcePage: navPage,
|
|
719
|
+
scope: navScope,
|
|
720
|
+
});
|
|
721
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
722
|
+
}
|
|
723
|
+
// RESUME — nextRunAt MUST be recomputed by the real scheduler's
|
|
724
|
+
// computeNextRun (cron / interval logic lives in scheduler.ts). We
|
|
725
|
+
// do a 2nd GET to read the freshly-recomputed row. If that GET
|
|
726
|
+
// fails or the row is missing, fall back to a `{...before, enabled:
|
|
727
|
+
// true}` synth — the row will still be functional, just with a
|
|
728
|
+
// possibly-stale nextRunAt for one render cycle. The next user
|
|
729
|
+
// interaction (refresh / back-to-list / another detail click) will
|
|
730
|
+
// converge it.
|
|
731
|
+
const postRefetch = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
732
|
+
let after;
|
|
733
|
+
if ('errorResult' in postRefetch) {
|
|
734
|
+
after = undefined;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
after = postRefetch.tasks.find(t => t.id === scheduleId);
|
|
738
|
+
}
|
|
739
|
+
if (!after) {
|
|
740
|
+
// Resume succeeded upstream but refetch could not surface the new row.
|
|
741
|
+
// Render a valid card; nextRunAt may be stale until the next refresh.
|
|
742
|
+
after = { ...before, enabled: true };
|
|
743
|
+
}
|
|
744
|
+
const detail = toScheduleDetailDto(after, { nowMs: now() });
|
|
745
|
+
const cardJson = buildSchedulesDetailCard(detail, {
|
|
746
|
+
invokerOpenId: operatorOpenId,
|
|
747
|
+
locale,
|
|
748
|
+
origin: navOrigin,
|
|
749
|
+
pageSize: navPageSize,
|
|
750
|
+
sourcePage: navPage,
|
|
751
|
+
scope: navScope,
|
|
752
|
+
});
|
|
753
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
754
|
+
}
|
|
755
|
+
// ─── 3c) DELIVERY — switch origin ↔ new-topic synchronously ──────────
|
|
756
|
+
if (action === SCHEDULES_ACTION_DELIVERY) {
|
|
757
|
+
const scheduleId = value.schedule_id;
|
|
758
|
+
if (typeof scheduleId !== 'string' || !scheduleId) {
|
|
759
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
760
|
+
}
|
|
761
|
+
const targetDelivery = value.target_delivery === 'origin' || value.target_delivery === 'new-topic'
|
|
762
|
+
? value.target_delivery
|
|
763
|
+
: undefined;
|
|
764
|
+
if (!targetDelivery) {
|
|
765
|
+
return ackToast('card.dashboard.settings.invalid_action', locale);
|
|
766
|
+
}
|
|
767
|
+
const failedKey = 'card.dashboard.schedules.delivery_failed';
|
|
768
|
+
const pre = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
769
|
+
if ('errorResult' in pre)
|
|
770
|
+
return pre.errorResult;
|
|
771
|
+
const before = pre.tasks.find(t => t.id === scheduleId);
|
|
772
|
+
if (!before) {
|
|
773
|
+
return errorToast('card.dashboard.schedules.schedule_not_found', undefined, locale);
|
|
774
|
+
}
|
|
775
|
+
// Server-side action matrix for delivery mirrors pause/resume: the card
|
|
776
|
+
// paint is UX only. Re-read the latest row and verify this exact target
|
|
777
|
+
// is still a legal transition before calling the owner daemon.
|
|
778
|
+
const deliveryState = computeDeliveryButtonAvailability(before, targetDelivery);
|
|
779
|
+
if (deliveryState.enabled !== true) {
|
|
780
|
+
const mappedKey = mapDeliveryDisabledReason(deliveryState.reasonKey) ?? failedKey;
|
|
781
|
+
return errorToast(mappedKey, undefined, locale);
|
|
782
|
+
}
|
|
783
|
+
let resp;
|
|
784
|
+
try {
|
|
785
|
+
resp = await client.request({
|
|
786
|
+
method: 'POST',
|
|
787
|
+
path: `/__daemon/schedules/${encodeURIComponent(scheduleId)}/delivery${writePathSuffix}`,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
catch (e) {
|
|
791
|
+
return errorToast(failedKey, { reason: e.message }, locale);
|
|
792
|
+
}
|
|
793
|
+
const failureReason = routeBWriteFailureReason(resp);
|
|
794
|
+
if (failureReason) {
|
|
795
|
+
return errorToast(failedKey, { reason: failureReason }, locale);
|
|
796
|
+
}
|
|
797
|
+
const responseDeliver = resp.body?.deliver === 'origin' ||
|
|
798
|
+
resp.body?.deliver === 'new-topic'
|
|
799
|
+
? resp.body.deliver
|
|
800
|
+
: targetDelivery;
|
|
801
|
+
const postRefetch = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
802
|
+
let after;
|
|
803
|
+
if (!('errorResult' in postRefetch)) {
|
|
804
|
+
after = postRefetch.tasks.find(t => t.id === scheduleId);
|
|
805
|
+
}
|
|
806
|
+
if (!after) {
|
|
807
|
+
// Delivery toggle succeeded upstream but the refetch was stale/missing.
|
|
808
|
+
// Keep the card interactive by rendering a synth row with the known
|
|
809
|
+
// target mode; the next refresh converges to the owner daemon snapshot.
|
|
810
|
+
after = { ...before, deliver: responseDeliver };
|
|
811
|
+
}
|
|
812
|
+
const detail = toScheduleDetailDto(after, { nowMs: now() });
|
|
813
|
+
const cardJson = buildSchedulesDetailCard(detail, {
|
|
814
|
+
invokerOpenId: operatorOpenId,
|
|
815
|
+
locale,
|
|
816
|
+
origin: navOrigin,
|
|
817
|
+
pageSize: navPageSize,
|
|
818
|
+
sourcePage: navPage,
|
|
819
|
+
scope: navScope,
|
|
820
|
+
});
|
|
821
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
822
|
+
}
|
|
823
|
+
// ─── 3d) BACK TO LIST — rebuild list card at the source page ─────────
|
|
824
|
+
if (action === SCHEDULES_ACTION_BACK_TO_LIST) {
|
|
825
|
+
const r = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
826
|
+
if ('errorResult' in r)
|
|
827
|
+
return r.errorResult;
|
|
828
|
+
const cardJson = buildSchedulesCard(r.tasks, {
|
|
829
|
+
invokerOpenId: operatorOpenId,
|
|
830
|
+
locale,
|
|
831
|
+
page: navPage ?? 1,
|
|
832
|
+
pageSize: navPageSize,
|
|
833
|
+
origin: navOrigin,
|
|
834
|
+
scope: navScope,
|
|
835
|
+
}, now());
|
|
836
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
837
|
+
}
|
|
838
|
+
// ─── 3e) REFRESH + PAGE actions ─────────────────────────────────────
|
|
839
|
+
// `action` is already constrained to validActions above; the only ones
|
|
840
|
+
// left here are REFRESH + PAGE (the other 4 returned early).
|
|
841
|
+
let page = 1;
|
|
842
|
+
if (action === SCHEDULES_ACTION_PAGE) {
|
|
843
|
+
// Page comes from value.page (prev/next button) OR action.option
|
|
844
|
+
// (select_static "jump to page" picker). Same action key, different
|
|
845
|
+
// dispatch field — handler converges on one branch.
|
|
846
|
+
const raw = value.page ?? data.action?.option ?? '1';
|
|
847
|
+
const parsed = Number.parseInt(raw, 10);
|
|
848
|
+
if (Number.isFinite(parsed) && parsed >= 1)
|
|
849
|
+
page = parsed;
|
|
850
|
+
}
|
|
851
|
+
const r = await safeGetSchedulesList(client, locale, listPathSuffix);
|
|
852
|
+
if ('errorResult' in r)
|
|
853
|
+
return r.errorResult;
|
|
854
|
+
const cardJson = buildSchedulesCard(r.tasks, {
|
|
855
|
+
invokerOpenId: operatorOpenId,
|
|
856
|
+
locale,
|
|
857
|
+
page,
|
|
858
|
+
pageSize: navPageSize,
|
|
859
|
+
origin: navOrigin,
|
|
860
|
+
scope: navScope,
|
|
861
|
+
}, now());
|
|
862
|
+
return {
|
|
863
|
+
card: { type: 'raw', data: JSON.parse(cardJson) },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* GET `/__daemon/schedules-list` and surface non-200 / network errors as
|
|
868
|
+
* caller-facing error toasts. Returns either `{ tasks }` or
|
|
869
|
+
* `{ errorResult }` — exactly one is set.
|
|
870
|
+
*
|
|
871
|
+
* createDaemonClient.request does NOT throw on 4xx/5xx — it resolves with
|
|
872
|
+
* the response. If we only catch throws we'd silently render an empty list
|
|
873
|
+
* when Route B returns 500/401, masking a real backend failure as "no
|
|
874
|
+
* schedules". So check `status !== 200` explicitly and surface as an
|
|
875
|
+
* error toast.
|
|
876
|
+
*/
|
|
877
|
+
async function safeGetSchedulesList(client, locale, pathSuffix = '') {
|
|
878
|
+
let r;
|
|
879
|
+
try {
|
|
880
|
+
r = await client.request({ method: 'GET', path: `/__daemon/schedules-list${pathSuffix}` });
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
return { errorResult: errorToast('card.dashboard.schedules.list_failed', { reason: e.message }, locale) };
|
|
884
|
+
}
|
|
885
|
+
if (r.status !== 200) {
|
|
886
|
+
const reason = String(r.body?.error ?? `http_${r.status}`);
|
|
887
|
+
return { errorResult: errorToast('card.dashboard.schedules.list_failed', { reason }, locale) };
|
|
888
|
+
}
|
|
889
|
+
const tasks = (r.body?.schedules) ?? [];
|
|
890
|
+
return { tasks };
|
|
891
|
+
}
|
|
892
|
+
//# sourceMappingURL=schedules-card.js.map
|