botmux 2.88.1 → 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/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 -269
- 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,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows 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 cancel and back actions.
|
|
6
|
+
*
|
|
7
|
+
* Cancel callbacks block on Route B and then refetch because a workflow can
|
|
8
|
+
* land in its terminal state asynchronously. If refetch fails or the row
|
|
9
|
+
* disappears, the card falls back to a cancelled-state synth so the user still
|
|
10
|
+
* gets a usable detail view, possibly stale for one render. Failures return
|
|
11
|
+
* toast-only.
|
|
12
|
+
*
|
|
13
|
+
* Security:
|
|
14
|
+
* - `invokerOpenId` pins callbacks to the admin who opened the card.
|
|
15
|
+
* - sender union_id never lands on `action.value`.
|
|
16
|
+
* - command entry and callbacks both enforce the dashboard admin gate.
|
|
17
|
+
* - row ids are routing keys only; Route B still enforces row ownership.
|
|
18
|
+
* - cancel re-runs the model availability matrix and owner-routability check
|
|
19
|
+
* before POSTing.
|
|
20
|
+
*/
|
|
21
|
+
import { isDashboardAdmin } from '../../dashboard/dashboard-admins.js';
|
|
22
|
+
import { computeActionAvailability, projectRunDetailDto, projectRunRowDto, } from '../../dashboard/workflow-card-model.js';
|
|
23
|
+
import { t } from '../../i18n/index.js';
|
|
24
|
+
export const WORKFLOWS_ACTION_REFRESH = 'dash_workflows_refresh';
|
|
25
|
+
export const WORKFLOWS_ACTION_PAGE = 'dash_workflows_page';
|
|
26
|
+
export const WORKFLOWS_ACTION_DETAIL = 'dash_workflows_detail';
|
|
27
|
+
export const WORKFLOWS_ACTION_CANCEL = 'dash_workflows_cancel';
|
|
28
|
+
export const WORKFLOWS_ACTION_BACK_TO_LIST = 'dash_workflows_back_to_list';
|
|
29
|
+
/** Action emitted by "🔙 返回总览" on overview-origin sub-cards. Same string
|
|
30
|
+
* as overview-card's OVERVIEW_ACTION_REFRESH (kept in sync; we don't import
|
|
31
|
+
* to avoid a circular dep). card-handler routes by action prefix, so
|
|
32
|
+
* dispatch lands on the overview handler regardless of which sub-card
|
|
33
|
+
* emitted it. */
|
|
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. Lark caps select
|
|
38
|
+
* options around this; we also keep payload small. Above the cap we fall
|
|
39
|
+
* back to prev/next only. */
|
|
40
|
+
const JUMP_PAGE_MAX_OPTIONS = 50;
|
|
41
|
+
/** Mapping from `StatusDot.tone` to a stable colour-emoji prefix. Pure. */
|
|
42
|
+
function toneIcon(tone) {
|
|
43
|
+
switch (tone) {
|
|
44
|
+
case 'success': return '🟢';
|
|
45
|
+
case 'info': return '🔵';
|
|
46
|
+
case 'warning': return '🟡';
|
|
47
|
+
case 'danger': return '🔴';
|
|
48
|
+
case 'neutral': return '⚪';
|
|
49
|
+
default: return '⚫';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Short status badge used after the title — '[running]' / '[failed]' etc. */
|
|
53
|
+
function statusBadge(status) {
|
|
54
|
+
return `[${status}]`;
|
|
55
|
+
}
|
|
56
|
+
/** Relative-time formatter: 'just now' / 'Ns ago' / 'Nm ago' / 'Nh ago' / 'Nd ago'. */
|
|
57
|
+
function formatRelative(fromMs, nowMs) {
|
|
58
|
+
if (typeof fromMs !== 'number' || !Number.isFinite(fromMs) || fromMs <= 0)
|
|
59
|
+
return '—';
|
|
60
|
+
const diff = nowMs - fromMs;
|
|
61
|
+
if (!Number.isFinite(diff) || diff < 0)
|
|
62
|
+
return 'just now';
|
|
63
|
+
const sec = Math.floor(diff / 1000);
|
|
64
|
+
if (sec < 60)
|
|
65
|
+
return `${sec}s ago`;
|
|
66
|
+
const min = Math.floor(sec / 60);
|
|
67
|
+
if (min < 60)
|
|
68
|
+
return `${min}m ago`;
|
|
69
|
+
const hour = Math.floor(min / 60);
|
|
70
|
+
if (hour < 24)
|
|
71
|
+
return `${hour}h ago`;
|
|
72
|
+
const day = Math.floor(hour / 24);
|
|
73
|
+
return `${day}d ago`;
|
|
74
|
+
}
|
|
75
|
+
/** Tally counts across the run pool.
|
|
76
|
+
*
|
|
77
|
+
* running = pending | running | waiting
|
|
78
|
+
* done = succeeded
|
|
79
|
+
* failed = failed | cancelled
|
|
80
|
+
*/
|
|
81
|
+
function countByStatus(rows) {
|
|
82
|
+
let running = 0, done = 0, failed = 0;
|
|
83
|
+
for (const r of rows) {
|
|
84
|
+
switch (r.status) {
|
|
85
|
+
case 'running':
|
|
86
|
+
case 'waiting':
|
|
87
|
+
case 'pending':
|
|
88
|
+
running += 1;
|
|
89
|
+
break;
|
|
90
|
+
case 'succeeded':
|
|
91
|
+
done += 1;
|
|
92
|
+
break;
|
|
93
|
+
case 'failed':
|
|
94
|
+
case 'cancelled':
|
|
95
|
+
failed += 1;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { running, done, failed };
|
|
100
|
+
}
|
|
101
|
+
/** Paginate without re-sorting; Route B owns the canonical workflow order. */
|
|
102
|
+
function paginate(items, page, pageSize) {
|
|
103
|
+
const total = items.length;
|
|
104
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
105
|
+
let active = Number.isFinite(page) ? Math.floor(page) : 1;
|
|
106
|
+
if (active < 1)
|
|
107
|
+
active = 1;
|
|
108
|
+
if (active > totalPages)
|
|
109
|
+
active = totalPages;
|
|
110
|
+
const start = (active - 1) * pageSize;
|
|
111
|
+
return {
|
|
112
|
+
items: items.slice(start, start + pageSize),
|
|
113
|
+
page: active,
|
|
114
|
+
totalPages,
|
|
115
|
+
total,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Build the workflows list card JSON. Pure (paginates + renders).
|
|
119
|
+
* Server-side listRuns order is preserved verbatim — no client-side resort.
|
|
120
|
+
* See module docblock for the rationale. */
|
|
121
|
+
export function buildWorkflowsCard(rows, opts, nowMs) {
|
|
122
|
+
const effectivePageSize = typeof opts.pageSize === 'number' && Number.isFinite(opts.pageSize) && opts.pageSize > 0
|
|
123
|
+
? Math.floor(opts.pageSize)
|
|
124
|
+
: PAGE_SIZE;
|
|
125
|
+
const { running, done, failed } = countByStatus(rows);
|
|
126
|
+
// Server order from listRuns is preserved verbatim — paginate directly.
|
|
127
|
+
const paged = paginate(rows, opts.page, effectivePageSize);
|
|
128
|
+
// Plumb origin + page_size + scope into every button.value so refresh/
|
|
129
|
+
// page/detail/detail-back rebuilds keep the same dashboard context.
|
|
130
|
+
const navFields = {};
|
|
131
|
+
if (opts.origin === 'overview')
|
|
132
|
+
navFields.origin = 'overview';
|
|
133
|
+
if (effectivePageSize !== PAGE_SIZE)
|
|
134
|
+
navFields.page_size = String(effectivePageSize);
|
|
135
|
+
if (opts.scope === 'global')
|
|
136
|
+
navFields.dashboard_scope = 'global';
|
|
137
|
+
const elements = [];
|
|
138
|
+
// Sub-header — counts + page indicator.
|
|
139
|
+
elements.push({
|
|
140
|
+
tag: 'div',
|
|
141
|
+
text: {
|
|
142
|
+
tag: 'lark_md',
|
|
143
|
+
content: t('card.dashboard.workflows.count_summary', {
|
|
144
|
+
running: String(running),
|
|
145
|
+
done: String(done),
|
|
146
|
+
failed: String(failed),
|
|
147
|
+
page: String(paged.page),
|
|
148
|
+
totalPages: String(paged.totalPages),
|
|
149
|
+
}, opts.locale),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
elements.push({ tag: 'hr' });
|
|
153
|
+
if (paged.items.length === 0) {
|
|
154
|
+
elements.push({
|
|
155
|
+
tag: 'div',
|
|
156
|
+
text: {
|
|
157
|
+
tag: 'lark_md',
|
|
158
|
+
content: t('card.dashboard.workflows.empty', undefined, opts.locale),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
for (const run of paged.items) {
|
|
164
|
+
const dto = projectRunRowDto(run);
|
|
165
|
+
elements.push(renderRow(dto, opts.locale, nowMs));
|
|
166
|
+
// Per-row action element holding ONLY the "📂 详情" button.
|
|
167
|
+
// Each row owns its own action block so its run_id can flow through
|
|
168
|
+
// `value.run_id` as the routing key.
|
|
169
|
+
elements.push({
|
|
170
|
+
tag: 'action',
|
|
171
|
+
actions: [
|
|
172
|
+
{
|
|
173
|
+
tag: 'button',
|
|
174
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.row_detail', undefined, opts.locale) },
|
|
175
|
+
type: 'default',
|
|
176
|
+
value: {
|
|
177
|
+
action: WORKFLOWS_ACTION_DETAIL,
|
|
178
|
+
invoker_open_id: opts.invokerOpenId,
|
|
179
|
+
run_id: dto.runId,
|
|
180
|
+
page: String(paged.page),
|
|
181
|
+
...navFields,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
elements.push({ tag: 'hr' });
|
|
189
|
+
// Pagination + refresh.
|
|
190
|
+
const actions = [];
|
|
191
|
+
if (paged.totalPages > 1) {
|
|
192
|
+
actions.push({
|
|
193
|
+
tag: 'button',
|
|
194
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.prev', undefined, opts.locale) },
|
|
195
|
+
type: 'default',
|
|
196
|
+
disabled: paged.page <= 1,
|
|
197
|
+
value: {
|
|
198
|
+
action: WORKFLOWS_ACTION_PAGE,
|
|
199
|
+
invoker_open_id: opts.invokerOpenId,
|
|
200
|
+
page: String(Math.max(1, paged.page - 1)),
|
|
201
|
+
...navFields,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
actions.push({
|
|
205
|
+
tag: 'button',
|
|
206
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.next', undefined, opts.locale) },
|
|
207
|
+
type: 'default',
|
|
208
|
+
disabled: paged.page >= paged.totalPages,
|
|
209
|
+
value: {
|
|
210
|
+
action: WORKFLOWS_ACTION_PAGE,
|
|
211
|
+
invoker_open_id: opts.invokerOpenId,
|
|
212
|
+
page: String(Math.min(paged.totalPages, paged.page + 1)),
|
|
213
|
+
...navFields,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
// "Jump to page" select — same action as prev/next, page comes via
|
|
217
|
+
// action.option instead of value.page. Handler reads `value.page ??
|
|
218
|
+
// action.option ?? '1'` so both paths converge on one branch. Capped at
|
|
219
|
+
// JUMP_PAGE_MAX_OPTIONS to keep payload small / inside Lark's option
|
|
220
|
+
// limit (above the cap, prev/next still works).
|
|
221
|
+
if (paged.totalPages > 2 && paged.totalPages <= JUMP_PAGE_MAX_OPTIONS) {
|
|
222
|
+
const options = Array.from({ length: paged.totalPages }, (_, i) => {
|
|
223
|
+
const n = i + 1;
|
|
224
|
+
return {
|
|
225
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.jump_page', { n: String(n), total: String(paged.totalPages) }, opts.locale) },
|
|
226
|
+
value: String(n),
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
actions.push({
|
|
230
|
+
tag: 'select_static',
|
|
231
|
+
placeholder: {
|
|
232
|
+
tag: 'plain_text',
|
|
233
|
+
content: t('card.dashboard.workflows.jump_page', { n: String(paged.page), total: String(paged.totalPages) }, opts.locale),
|
|
234
|
+
},
|
|
235
|
+
initial_option: String(paged.page),
|
|
236
|
+
options,
|
|
237
|
+
value: {
|
|
238
|
+
action: WORKFLOWS_ACTION_PAGE,
|
|
239
|
+
invoker_open_id: opts.invokerOpenId,
|
|
240
|
+
...navFields,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
actions.push({
|
|
246
|
+
tag: 'button',
|
|
247
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.refresh', undefined, opts.locale) },
|
|
248
|
+
type: 'default',
|
|
249
|
+
value: {
|
|
250
|
+
action: WORKFLOWS_ACTION_REFRESH,
|
|
251
|
+
invoker_open_id: opts.invokerOpenId,
|
|
252
|
+
...navFields,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
// Overview drilldown only — "🔙 返回总览" reuses the overview-refresh
|
|
256
|
+
// action; card-handler routes by action prefix, so dispatch lands on
|
|
257
|
+
// overview-card.ts which rebuilds the parent card cleanly.
|
|
258
|
+
if (opts.origin === 'overview') {
|
|
259
|
+
actions.push({
|
|
260
|
+
tag: 'button',
|
|
261
|
+
text: { tag: 'plain_text', content: t('card.dashboard.overview.back_button', undefined, opts.locale) },
|
|
262
|
+
type: 'default',
|
|
263
|
+
value: {
|
|
264
|
+
action: BACK_TO_OVERVIEW_ACTION,
|
|
265
|
+
invoker_open_id: opts.invokerOpenId,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
elements.push({ tag: 'action', actions });
|
|
270
|
+
elements.push({
|
|
271
|
+
tag: 'note',
|
|
272
|
+
elements: [
|
|
273
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
return JSON.stringify({
|
|
277
|
+
config: { wide_screen_mode: true },
|
|
278
|
+
header: {
|
|
279
|
+
title: { tag: 'plain_text', content: t('card.dashboard.workflows.title', undefined, opts.locale) },
|
|
280
|
+
template: 'blue',
|
|
281
|
+
},
|
|
282
|
+
elements,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function renderRow(row, locale, nowMs) {
|
|
286
|
+
const icon = toneIcon(row.dot.tone);
|
|
287
|
+
// Primary: status icon + bold (workflowId || runId[0..8]) + ' ' + [status]
|
|
288
|
+
const primaryLabel = row.workflowId && row.workflowId.length > 0
|
|
289
|
+
? row.workflowId
|
|
290
|
+
: row.runId.slice(0, 8);
|
|
291
|
+
const badge = statusBadge(row.status);
|
|
292
|
+
// Secondary: progress + started + updated. Only show if at least one is present.
|
|
293
|
+
const raw = row.raw;
|
|
294
|
+
const secondaryParts = [];
|
|
295
|
+
if (typeof raw.nodesDone === 'number' && Number.isFinite(raw.nodesDone) &&
|
|
296
|
+
typeof raw.nodesTotal === 'number' && Number.isFinite(raw.nodesTotal) && raw.nodesTotal > 0) {
|
|
297
|
+
secondaryParts.push(t('card.dashboard.workflows.progress_label', {
|
|
298
|
+
done: String(raw.nodesDone),
|
|
299
|
+
total: String(raw.nodesTotal),
|
|
300
|
+
}, locale));
|
|
301
|
+
}
|
|
302
|
+
if (typeof raw.startedAt === 'number' && Number.isFinite(raw.startedAt)) {
|
|
303
|
+
secondaryParts.push(t('card.dashboard.workflows.started_label', { rel: formatRelative(raw.startedAt, nowMs) }, locale));
|
|
304
|
+
}
|
|
305
|
+
if (typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt)) {
|
|
306
|
+
secondaryParts.push(t('card.dashboard.workflows.updated_label', { rel: formatRelative(raw.updatedAt, nowMs) }, locale));
|
|
307
|
+
}
|
|
308
|
+
const secondary = secondaryParts.length > 0
|
|
309
|
+
? `\n<font color="grey">${escapeLarkMd(secondaryParts.join(' · '))}</font>`
|
|
310
|
+
: '';
|
|
311
|
+
return {
|
|
312
|
+
tag: 'div',
|
|
313
|
+
text: {
|
|
314
|
+
tag: 'lark_md',
|
|
315
|
+
content: `${icon} **${escapeLarkMd(primaryLabel)}** ${escapeLarkMd(badge)}` + secondary,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build the workflow detail card: run metadata, optional node progress, cancel
|
|
321
|
+
* action, and back action.
|
|
322
|
+
*
|
|
323
|
+
* The cancel button is disabled when EITHER:
|
|
324
|
+
* - `computeActionAvailability(status).cancel.enabled === false`
|
|
325
|
+
* (terminal status: succeeded / failed / cancelled), OR
|
|
326
|
+
* - `chatBinding.larkAppId` is missing — no routable owner for Route B to
|
|
327
|
+
* proxy to. The `runCancel` helper would 409 `needs_cli_cancel` here, so
|
|
328
|
+
* we fail-closed at the UI layer too.
|
|
329
|
+
* Both disabled reasons surface as inline notes next to the button.
|
|
330
|
+
*/
|
|
331
|
+
export function buildWorkflowsDetailCard(detail, opts) {
|
|
332
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
333
|
+
const elements = [];
|
|
334
|
+
// ─── Title — status dot + bold workflow label + runId monospace ────────
|
|
335
|
+
const icon = toneIcon(detail.dot.tone);
|
|
336
|
+
const titleLabel = detail.workflowId && detail.workflowId.length > 0
|
|
337
|
+
? detail.workflowId
|
|
338
|
+
: detail.runId.slice(0, 8);
|
|
339
|
+
elements.push({
|
|
340
|
+
tag: 'div',
|
|
341
|
+
text: {
|
|
342
|
+
tag: 'lark_md',
|
|
343
|
+
content: `${icon} **${escapeLarkMd(titleLabel)}** ${escapeLarkMd(statusBadge(detail.status))}` +
|
|
344
|
+
`\n\`${escapeLarkMd(detail.runId)}\``,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
elements.push({ tag: 'hr' });
|
|
348
|
+
// ─── Key/value block ───────────────────────────────────────────────────
|
|
349
|
+
const infoLines = [];
|
|
350
|
+
if (detail.workflowId) {
|
|
351
|
+
infoLines.push(t('card.dashboard.workflows.detail.workflow_label', { workflowId: escapeLarkMd(detail.workflowId) }, opts.locale));
|
|
352
|
+
}
|
|
353
|
+
infoLines.push(t('card.dashboard.workflows.detail.run_label', { runId: escapeLarkMd(detail.runId) }, opts.locale));
|
|
354
|
+
infoLines.push(t('card.dashboard.workflows.detail.status_label', { status: escapeLarkMd(detail.status) }, opts.locale));
|
|
355
|
+
infoLines.push(t('card.dashboard.workflows.detail.started_label', { rel: escapeLarkMd(formatRelative(detail.startedAtMs, nowMs)) }, opts.locale));
|
|
356
|
+
infoLines.push(t('card.dashboard.workflows.detail.updated_label', { rel: escapeLarkMd(formatRelative(detail.updatedAtMs, nowMs)) }, opts.locale));
|
|
357
|
+
// finished_at only surfaces on terminal states.
|
|
358
|
+
if (typeof detail.finishedAtMs === 'number' && Number.isFinite(detail.finishedAtMs)) {
|
|
359
|
+
infoLines.push(t('card.dashboard.workflows.detail.finished_label', { rel: escapeLarkMd(formatRelative(detail.finishedAtMs, nowMs)) }, opts.locale));
|
|
360
|
+
}
|
|
361
|
+
if (detail.elapsedLabel) {
|
|
362
|
+
infoLines.push(t('card.dashboard.workflows.detail.elapsed_label', { elapsed: escapeLarkMd(detail.elapsedLabel) }, opts.locale));
|
|
363
|
+
}
|
|
364
|
+
if (detail.progressLabel) {
|
|
365
|
+
infoLines.push(t('card.dashboard.workflows.detail.progress_label', { progress: escapeLarkMd(detail.progressLabel) }, opts.locale));
|
|
366
|
+
}
|
|
367
|
+
if (detail.chatBinding) {
|
|
368
|
+
const chatId = detail.chatBinding.chatId ?? '';
|
|
369
|
+
const larkAppId = detail.chatBinding.larkAppId ?? '';
|
|
370
|
+
const chatLabel = larkAppId
|
|
371
|
+
? `${chatId} · ${larkAppId}`
|
|
372
|
+
: chatId;
|
|
373
|
+
if (chatLabel) {
|
|
374
|
+
infoLines.push(t('card.dashboard.workflows.detail.chat_label', { chat: escapeLarkMd(chatLabel) }, opts.locale));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
elements.push({
|
|
378
|
+
tag: 'div',
|
|
379
|
+
text: {
|
|
380
|
+
tag: 'lark_md',
|
|
381
|
+
content: infoLines.map(l => `<font color="grey">${l}</font>`).join('\n'),
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
// ─── Nodes section (if any) ────────────────────────────────────────────
|
|
385
|
+
if (detail.nodes.length > 0) {
|
|
386
|
+
elements.push({ tag: 'hr' });
|
|
387
|
+
elements.push({
|
|
388
|
+
tag: 'div',
|
|
389
|
+
text: {
|
|
390
|
+
tag: 'lark_md',
|
|
391
|
+
content: `**${t('card.dashboard.workflows.detail.nodes_header', undefined, opts.locale)}**\n` +
|
|
392
|
+
detail.nodes.map(n => {
|
|
393
|
+
const name = n.name ?? n.nodeId;
|
|
394
|
+
const statusBit = n.status ? ` [${escapeLarkMd(n.status)}]` : '';
|
|
395
|
+
return `- ${n.index}. ${escapeLarkMd(name)}${statusBit}`;
|
|
396
|
+
}).join('\n'),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
elements.push({ tag: 'hr' });
|
|
401
|
+
// ─── Action row — cancel (danger w/ confirm) + back ────────────────────
|
|
402
|
+
// Two-condition disable: terminal status OR missing routable owner.
|
|
403
|
+
const matrixAllows = detail.actions.cancel.enabled === true;
|
|
404
|
+
const hasOwner = typeof detail.chatBinding?.larkAppId === 'string' && detail.chatBinding.larkAppId.length > 0;
|
|
405
|
+
const cancelEnabled = matrixAllows && hasOwner;
|
|
406
|
+
// Threaded nav state — successful cancel rebuilds this same detail card,
|
|
407
|
+
// and the user may then press 🔙 返回 to go back to the drilldown list,
|
|
408
|
+
// which still needs origin/pageSize. So plumb the same fields onto both
|
|
409
|
+
// the cancel button (round-trip preservation) and the back button.
|
|
410
|
+
const backNav = {};
|
|
411
|
+
if (opts.origin === 'overview')
|
|
412
|
+
backNav.origin = 'overview';
|
|
413
|
+
if (typeof opts.sourcePage === 'number' && Number.isFinite(opts.sourcePage) && opts.sourcePage >= 1) {
|
|
414
|
+
backNav.page = String(Math.floor(opts.sourcePage));
|
|
415
|
+
}
|
|
416
|
+
if (typeof opts.pageSize === 'number'
|
|
417
|
+
&& Number.isFinite(opts.pageSize)
|
|
418
|
+
&& opts.pageSize > 0
|
|
419
|
+
&& opts.pageSize !== PAGE_SIZE) {
|
|
420
|
+
backNav.page_size = String(Math.floor(opts.pageSize));
|
|
421
|
+
}
|
|
422
|
+
if (opts.scope === 'global')
|
|
423
|
+
backNav.dashboard_scope = 'global';
|
|
424
|
+
const cancelButton = {
|
|
425
|
+
tag: 'button',
|
|
426
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.btn.cancel', undefined, opts.locale) },
|
|
427
|
+
type: 'danger',
|
|
428
|
+
value: {
|
|
429
|
+
action: WORKFLOWS_ACTION_CANCEL,
|
|
430
|
+
invoker_open_id: opts.invokerOpenId,
|
|
431
|
+
run_id: detail.runId,
|
|
432
|
+
...backNav,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
if (cancelEnabled) {
|
|
436
|
+
// Feishu V1 card schema confirm dialog. Only attach when the button is
|
|
437
|
+
// actually clickable — there is no value in confirming a disabled button.
|
|
438
|
+
cancelButton.confirm = {
|
|
439
|
+
title: { tag: 'plain_text', content: t('card.dashboard.workflows.confirm.cancel.title', undefined, opts.locale) },
|
|
440
|
+
text: {
|
|
441
|
+
tag: 'plain_text',
|
|
442
|
+
content: t('card.dashboard.workflows.confirm.cancel.text', { runId: detail.runId }, opts.locale),
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
cancelButton.disabled = true;
|
|
448
|
+
}
|
|
449
|
+
elements.push({
|
|
450
|
+
tag: 'action',
|
|
451
|
+
actions: [
|
|
452
|
+
cancelButton,
|
|
453
|
+
{
|
|
454
|
+
tag: 'button',
|
|
455
|
+
text: { tag: 'plain_text', content: t('card.dashboard.workflows.btn.back', undefined, opts.locale) },
|
|
456
|
+
type: 'default',
|
|
457
|
+
value: {
|
|
458
|
+
action: WORKFLOWS_ACTION_BACK_TO_LIST,
|
|
459
|
+
invoker_open_id: opts.invokerOpenId,
|
|
460
|
+
...backNav,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
// Surface inline reason next to a disabled cancel button. Owner-missing
|
|
466
|
+
// takes precedence over terminal — a terminal run with no owner is still
|
|
467
|
+
// primarily "no routable owner" from the dashboard's POV. The mapped key
|
|
468
|
+
// is one of:
|
|
469
|
+
// card.dashboard.workflows.cancel.disabled.alreadyTerminal
|
|
470
|
+
// card.dashboard.workflows.cancel.disabled.noOwner
|
|
471
|
+
if (!cancelEnabled) {
|
|
472
|
+
const reasonKey = !hasOwner
|
|
473
|
+
? 'card.dashboard.workflows.cancel.disabled.noOwner'
|
|
474
|
+
: mapCancelDisabledReason(detail.actions.cancel.reasonKey);
|
|
475
|
+
if (reasonKey) {
|
|
476
|
+
elements.push({
|
|
477
|
+
tag: 'note',
|
|
478
|
+
elements: [
|
|
479
|
+
{ tag: 'lark_md', content: t(reasonKey, undefined, opts.locale) },
|
|
480
|
+
],
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Footer security note (mirrors list card).
|
|
485
|
+
elements.push({
|
|
486
|
+
tag: 'note',
|
|
487
|
+
elements: [
|
|
488
|
+
{ tag: 'lark_md', content: t('card.dashboard.settings.footer.security', undefined, opts.locale) },
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
return JSON.stringify({
|
|
492
|
+
config: { wide_screen_mode: true },
|
|
493
|
+
header: {
|
|
494
|
+
title: { tag: 'plain_text', content: t('card.dashboard.workflows.detail.title', undefined, opts.locale) },
|
|
495
|
+
template: 'blue',
|
|
496
|
+
},
|
|
497
|
+
elements,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/** Map cancel-disabled reason keys to card i18n keys. */
|
|
501
|
+
function mapCancelDisabledReason(reasonKey) {
|
|
502
|
+
switch (reasonKey) {
|
|
503
|
+
case 'workflows.action.cancel.terminal':
|
|
504
|
+
return 'card.dashboard.workflows.cancel.disabled.alreadyTerminal';
|
|
505
|
+
default:
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Sanitize user-/CLI-supplied text for inclusion in lark_md. See
|
|
511
|
+
* `sessions-card.ts:escapeLarkMd` for the order rationale: `&` first to
|
|
512
|
+
* avoid `&` → `&` then `<` → `<` getting re-encoded as `&lt;`.
|
|
513
|
+
*/
|
|
514
|
+
function escapeLarkMd(text) {
|
|
515
|
+
return text
|
|
516
|
+
.replace(/&/g, '&')
|
|
517
|
+
.replace(/</g, '<')
|
|
518
|
+
.replace(/>/g, '>')
|
|
519
|
+
.replace(/([*_~`])/g, '\\$1');
|
|
520
|
+
}
|
|
521
|
+
function ackToast(textKey, locale) {
|
|
522
|
+
return { toast: { type: 'info', content: t(textKey, undefined, locale) } };
|
|
523
|
+
}
|
|
524
|
+
function errorToast(textKey, params, locale) {
|
|
525
|
+
return { toast: { type: 'error', content: t(textKey, params, locale) } };
|
|
526
|
+
}
|
|
527
|
+
/** Dispatch a `dash_workflows_*` action callback. */
|
|
528
|
+
export async function handleWorkflowsCardAction(data, larkAppId, deps) {
|
|
529
|
+
const locale = deps.locale ?? 'zh';
|
|
530
|
+
const value = (data.action?.value ?? {});
|
|
531
|
+
const operatorOpenId = data.operator?.open_id;
|
|
532
|
+
const action = value.action;
|
|
533
|
+
// ─── 1) Invoker lock — fail-closed ──────────────────────────────────
|
|
534
|
+
const invokerOpenId = value.invoker_open_id;
|
|
535
|
+
if (typeof invokerOpenId !== 'string' || !invokerOpenId) {
|
|
536
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
537
|
+
}
|
|
538
|
+
if (typeof operatorOpenId !== 'string' || !operatorOpenId) {
|
|
539
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
540
|
+
}
|
|
541
|
+
if (invokerOpenId !== operatorOpenId) {
|
|
542
|
+
return ackToast('card.dashboard.settings.not_invoker', locale);
|
|
543
|
+
}
|
|
544
|
+
// ─── 2) Per-bot admin gate ──────────────────────────────────────────
|
|
545
|
+
if (!isDashboardAdmin(larkAppId, operatorOpenId, deps)) {
|
|
546
|
+
return ackToast('card.dashboard.settings.owner_only', locale);
|
|
547
|
+
}
|
|
548
|
+
// Validate the action BEFORE creating the Route B client — an unknown
|
|
549
|
+
// action shouldn't even open a connection.
|
|
550
|
+
const validActions = new Set([
|
|
551
|
+
WORKFLOWS_ACTION_REFRESH,
|
|
552
|
+
WORKFLOWS_ACTION_PAGE,
|
|
553
|
+
WORKFLOWS_ACTION_DETAIL,
|
|
554
|
+
WORKFLOWS_ACTION_CANCEL,
|
|
555
|
+
WORKFLOWS_ACTION_BACK_TO_LIST,
|
|
556
|
+
]);
|
|
557
|
+
if (!validActions.has(action)) {
|
|
558
|
+
return ackToast('card.dashboard.settings.invalid_action', locale);
|
|
559
|
+
}
|
|
560
|
+
const client = deps.createClient(larkAppId);
|
|
561
|
+
const now = () => (deps.nowMs ? deps.nowMs() : Date.now());
|
|
562
|
+
// ─── Nav state (overview drilldown) ─────────────────────────────────
|
|
563
|
+
// Threaded by buildWorkflowsCard onto every button.value; we parse here
|
|
564
|
+
// so the rebuild path keeps the same shape (5/page + 🔙 返回总览).
|
|
565
|
+
const navOrigin = value.origin === 'overview' ? 'overview' : undefined;
|
|
566
|
+
const parsedPageSize = Number.parseInt(value.page_size ?? '', 10);
|
|
567
|
+
const navPageSize = Number.isFinite(parsedPageSize) && parsedPageSize > 0 ? parsedPageSize : undefined;
|
|
568
|
+
const parsedNavPage = Number.parseInt(value.page ?? '', 10);
|
|
569
|
+
const navPage = Number.isFinite(parsedNavPage) && parsedNavPage >= 1 ? parsedNavPage : undefined;
|
|
570
|
+
const navScope = value.dashboard_scope === 'global' ? 'global' : undefined;
|
|
571
|
+
const listPathSuffix = navScope === 'global' ? '&scope=global' : '';
|
|
572
|
+
const writePathSuffix = navScope === 'global' ? '?scope=global' : '';
|
|
573
|
+
// ─── 3a) DETAIL — open the per-run detail card ──────────────────────
|
|
574
|
+
if (action === WORKFLOWS_ACTION_DETAIL) {
|
|
575
|
+
const runId = value.run_id;
|
|
576
|
+
if (typeof runId !== 'string' || !runId) {
|
|
577
|
+
return errorToast('card.dashboard.workflows.workflow_not_found', undefined, locale);
|
|
578
|
+
}
|
|
579
|
+
const r = await safeGetWorkflowsList(client, locale, listPathSuffix);
|
|
580
|
+
if ('errorResult' in r)
|
|
581
|
+
return r.errorResult;
|
|
582
|
+
const row = r.runs.find(x => x.runId === runId);
|
|
583
|
+
if (!row) {
|
|
584
|
+
return errorToast('card.dashboard.workflows.workflow_not_found', undefined, locale);
|
|
585
|
+
}
|
|
586
|
+
const detail = projectRunDetailDto(row, { nowMs: now() });
|
|
587
|
+
const cardJson = buildWorkflowsDetailCard(detail, {
|
|
588
|
+
invokerOpenId: operatorOpenId,
|
|
589
|
+
locale,
|
|
590
|
+
nowMs: now(),
|
|
591
|
+
origin: navOrigin,
|
|
592
|
+
pageSize: navPageSize,
|
|
593
|
+
sourcePage: navPage,
|
|
594
|
+
scope: navScope,
|
|
595
|
+
});
|
|
596
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
597
|
+
}
|
|
598
|
+
// ─── 3b) CANCEL — refetch-based cancel, then redraw detail ──────────
|
|
599
|
+
if (action === WORKFLOWS_ACTION_CANCEL) {
|
|
600
|
+
const runId = value.run_id;
|
|
601
|
+
if (typeof runId !== 'string' || !runId) {
|
|
602
|
+
return errorToast('card.dashboard.workflows.workflow_not_found', undefined, locale);
|
|
603
|
+
}
|
|
604
|
+
// Pre-POST snapshot confirms the run still exists and lets us re-run the
|
|
605
|
+
// action matrix plus owner-routability checks against fresh state.
|
|
606
|
+
const pre = await safeGetWorkflowsList(client, locale, listPathSuffix);
|
|
607
|
+
if ('errorResult' in pre)
|
|
608
|
+
return pre.errorResult;
|
|
609
|
+
const before = pre.runs.find(x => x.runId === runId);
|
|
610
|
+
if (!before) {
|
|
611
|
+
return errorToast('card.dashboard.workflows.workflow_not_found', undefined, locale);
|
|
612
|
+
}
|
|
613
|
+
// Both disabled conditions fail closed before POSTing:
|
|
614
|
+
// (a) the model matrix allows cancel for this status,
|
|
615
|
+
// (b) chatBinding.larkAppId is present so Route B can route to an owner.
|
|
616
|
+
// Surface the same i18n keys the builder uses for disabled-button notes.
|
|
617
|
+
const availability = computeActionAvailability(before.status);
|
|
618
|
+
if (availability.cancel.enabled !== true) {
|
|
619
|
+
const mappedKey = mapCancelDisabledReason(availability.cancel.reasonKey)
|
|
620
|
+
?? 'card.dashboard.workflows.cancel.disabled.alreadyTerminal';
|
|
621
|
+
return errorToast(mappedKey, undefined, locale);
|
|
622
|
+
}
|
|
623
|
+
if (typeof before.chatBinding?.larkAppId !== 'string' || before.chatBinding.larkAppId.length === 0) {
|
|
624
|
+
return errorToast('card.dashboard.workflows.cancel.disabled.noOwner', undefined, locale);
|
|
625
|
+
}
|
|
626
|
+
// POST cancel. Route B owner routing is the authority on whether this
|
|
627
|
+
// run can be cancelled; the IM layer only sanitizes the
|
|
628
|
+
// routing key + does the matrix check above.
|
|
629
|
+
let resp;
|
|
630
|
+
try {
|
|
631
|
+
resp = await client.request({
|
|
632
|
+
method: 'POST',
|
|
633
|
+
path: `/__daemon/workflows-runs/${encodeURIComponent(runId)}/cancel${writePathSuffix}`,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (e) {
|
|
637
|
+
return errorToast('card.dashboard.workflows.cancel_failed', { reason: e.message }, locale);
|
|
638
|
+
}
|
|
639
|
+
if (resp.status !== 200) {
|
|
640
|
+
const body = (resp.body ?? {});
|
|
641
|
+
const reason = String(body.error ?? `http_${resp.status}`);
|
|
642
|
+
// Preserve user state — do NOT redraw card on failure.
|
|
643
|
+
return errorToast('card.dashboard.workflows.cancel_failed', { reason }, locale);
|
|
644
|
+
}
|
|
645
|
+
// Cancel may land terminal on a separate tick. Refetch and re-project the
|
|
646
|
+
// fresh row; fall back to a possibly stale cancelled synth if the row is
|
|
647
|
+
// temporarily absent.
|
|
648
|
+
const postRefetch = await safeGetWorkflowsList(client, locale, listPathSuffix);
|
|
649
|
+
let after;
|
|
650
|
+
if ('errorResult' in postRefetch) {
|
|
651
|
+
after = undefined;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
after = postRefetch.runs.find(x => x.runId === runId);
|
|
655
|
+
}
|
|
656
|
+
if (!after) {
|
|
657
|
+
after = { ...before, status: 'cancelled' };
|
|
658
|
+
}
|
|
659
|
+
const detail = projectRunDetailDto(after, { nowMs: now() });
|
|
660
|
+
const cardJson = buildWorkflowsDetailCard(detail, {
|
|
661
|
+
invokerOpenId: operatorOpenId,
|
|
662
|
+
locale,
|
|
663
|
+
nowMs: now(),
|
|
664
|
+
origin: navOrigin,
|
|
665
|
+
pageSize: navPageSize,
|
|
666
|
+
sourcePage: navPage,
|
|
667
|
+
scope: navScope,
|
|
668
|
+
});
|
|
669
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
670
|
+
}
|
|
671
|
+
// ─── 3c) BACK TO LIST — rebuild list card at the source page ─────────
|
|
672
|
+
if (action === WORKFLOWS_ACTION_BACK_TO_LIST) {
|
|
673
|
+
const r = await safeGetWorkflowsList(client, locale, listPathSuffix);
|
|
674
|
+
if ('errorResult' in r)
|
|
675
|
+
return r.errorResult;
|
|
676
|
+
const cardJson = buildWorkflowsCard(r.runs, {
|
|
677
|
+
invokerOpenId: operatorOpenId,
|
|
678
|
+
locale,
|
|
679
|
+
page: navPage ?? 1,
|
|
680
|
+
pageSize: navPageSize,
|
|
681
|
+
origin: navOrigin,
|
|
682
|
+
scope: navScope,
|
|
683
|
+
}, now());
|
|
684
|
+
return { card: { type: 'raw', data: JSON.parse(cardJson) } };
|
|
685
|
+
}
|
|
686
|
+
// ─── 3d) REFRESH + PAGE actions ───────────────────────────
|
|
687
|
+
// `action` is already constrained to validActions above; the only ones
|
|
688
|
+
// left here are REFRESH + PAGE (the other 3 returned early).
|
|
689
|
+
let page = 1;
|
|
690
|
+
if (action === WORKFLOWS_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
|
+
const r = await safeGetWorkflowsList(client, locale, listPathSuffix);
|
|
700
|
+
if ('errorResult' in r)
|
|
701
|
+
return r.errorResult;
|
|
702
|
+
const cardJson = buildWorkflowsCard(r.runs, {
|
|
703
|
+
invokerOpenId: operatorOpenId,
|
|
704
|
+
locale,
|
|
705
|
+
page,
|
|
706
|
+
pageSize: navPageSize,
|
|
707
|
+
origin: navOrigin,
|
|
708
|
+
scope: navScope,
|
|
709
|
+
}, now());
|
|
710
|
+
return {
|
|
711
|
+
card: { type: 'raw', data: JSON.parse(cardJson) },
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* GET `/__daemon/workflows-runs-snapshot?all=1` and surface non-200 / network
|
|
716
|
+
* errors as caller-facing error toasts. `?all=1` is required because default
|
|
717
|
+
* listRuns hides terminal runs (succeeded/failed/cancelled), so the
|
|
718
|
+
* counters and the detail/cancel paths would miss recently-terminated runs
|
|
719
|
+
* without it. Returns either `{ runs }` or `{ errorResult }` — exactly one
|
|
720
|
+
* is set.
|
|
721
|
+
*/
|
|
722
|
+
async function safeGetWorkflowsList(client, locale, pathSuffix = '') {
|
|
723
|
+
let r;
|
|
724
|
+
try {
|
|
725
|
+
r = await client.request({ method: 'GET', path: `/__daemon/workflows-runs-snapshot?all=1${pathSuffix}` });
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
return { errorResult: errorToast('card.dashboard.workflows.list_failed', { reason: e.message }, locale) };
|
|
729
|
+
}
|
|
730
|
+
if (r.status !== 200) {
|
|
731
|
+
const reason = String(r.body?.error ?? `http_${r.status}`);
|
|
732
|
+
return { errorResult: errorToast('card.dashboard.workflows.list_failed', { reason }, locale) };
|
|
733
|
+
}
|
|
734
|
+
const runs = (r.body?.runs) ?? [];
|
|
735
|
+
return { runs };
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=workflows-card.js.map
|