@mldong/jeeflow 1.8.23 → 1.8.26

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/engine.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ProcessInstance } from './model.js';
2
2
  import type { ProcessRepository, UserProvider, IDGenerator, ExpressionEvaluator } from './spi.js';
3
- import { type EngineExtensions } from './extensions.js';
3
+ import { type EngineExtensions, type ProcessEvent } from './extensions.js';
4
4
  import { HandlerRegistry } from './registry.js';
5
5
  export declare const KeySubmitType = "submitType";
6
6
  export declare const KeyBusinessNo = "BUSINESS_NO";
@@ -23,6 +23,7 @@ export interface Engine {
23
23
  executeAndJumpToFirstTaskNode(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
24
24
  }
25
25
  export declare class EngineImpl implements Engine {
26
+ #private;
26
27
  private repo;
27
28
  private userProv?;
28
29
  private idGen?;
@@ -43,7 +44,7 @@ export declare class EngineImpl implements Engine {
43
44
  /** 表达式求值(v1.5.0,门面 highLight 决策分支过滤用) */
44
45
  evalExpr(expr: string, vars: Record<string, any>): Promise<any>;
45
46
  private firePost;
46
- private fireEvent;
47
+ fireEvent(evt: ProcessEvent): Promise<void>;
47
48
  startProcessInstanceById(defineId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
48
49
  executeProcessTask(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
49
50
  executeAndJumpToEnd(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
package/dist/engine.js CHANGED
@@ -97,10 +97,23 @@ export class EngineImpl {
97
97
  await ic.postHandle(node, inst);
98
98
  }
99
99
  async fireEvent(evt) {
100
+ // 公开事件发布入口(issues/102 CC_CREATE):facade 层 CC 实例创建后逐抄送人 fire;
101
+ // 无监听器(ext/listeners 为空)时零副作用,与上一版逐字节一致
102
+ await this.#fireEvent(evt);
103
+ }
104
+ async #fireEvent(evt) {
100
105
  if (!this.ext?.listeners)
101
106
  return;
102
- for (const l of this.ext.listeners)
103
- await l(evt);
107
+ // 兜底语义(issues/104 P2 统一口径):单监听器异常只记录不传播——
108
+ // 不得影响引擎主流程,也不得中断后续监听器(对齐 PHP per-listener catch)
109
+ for (const l of this.ext.listeners) {
110
+ try {
111
+ await l(evt);
112
+ }
113
+ catch (e) {
114
+ console.error(`[jeeflow] process event listener error: type=${evt.type} instanceId=${evt.instanceId}`, e);
115
+ }
116
+ }
104
117
  }
105
118
  // ─── Start ─────────────────────────────────────────────────────────────────
106
119
  async startProcessInstanceById(defineId, operator, args = {}) {
@@ -22,7 +22,8 @@ export declare enum EventType {
22
22
  ProcessFinish = 1,
23
23
  ProcessReject = 2,
24
24
  TaskCreate = 3,
25
- TaskComplete = 4
25
+ TaskComplete = 4,
26
+ CcCreate = 5
26
27
  }
27
28
  export interface ProcessEvent {
28
29
  type: EventType;
@@ -30,6 +31,8 @@ export interface ProcessEvent {
30
31
  taskId?: string;
31
32
  nodeId?: string;
32
33
  operator: string;
34
+ /** 抄送人 id 直传事件体,监听器免反查 cc 表(issues/102;对齐 Java ccActorId / Go CcActorID) */
35
+ ccActorId?: string;
33
36
  }
34
37
  export type ProcessEventListener = (event: ProcessEvent) => void | Promise<void>;
35
38
  export interface EngineExtensions {
@@ -5,4 +5,7 @@ export var EventType;
5
5
  EventType[EventType["ProcessReject"] = 2] = "ProcessReject";
6
6
  EventType[EventType["TaskCreate"] = 3] = "TaskCreate";
7
7
  EventType[EventType["TaskComplete"] = 4] = "TaskComplete";
8
+ // issues/102:抄送知会(对齐 Java CC_CREATE / Go EventCCCreate / Python CC_CREATE / PHP CC_CREATE;
9
+ // 4 号位是活码 TaskComplete,0/1/2/3/4 不重排——码值契约 Go/Node=5)
10
+ EventType[EventType["CcCreate"] = 5] = "CcCreate";
8
11
  })(EventType || (EventType = {}));
package/dist/facade.d.ts CHANGED
@@ -87,4 +87,11 @@ export declare class JeeflowFacade {
87
87
  private doneList;
88
88
  /** 定义 content 解析为 LogicFlow JSON(issues/05 jsonObject) */
89
89
  private parseGraph;
90
+ private static readonly DEFAULT_STATE_IN;
91
+ private static readonly DEFAULT_STATS_LIMIT;
92
+ private static readonly VALID_GRANULARITY;
93
+ private static readonly VALID_DIMENSION;
94
+ private statsOverview;
95
+ private statsTrend;
96
+ private statsGroup;
90
97
  }
package/dist/facade.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // (code=0 成功 / 99999999 失败)。操作人约定:args.operator 显式传入。
6
6
  import { TaskState, } from './model.js';
7
7
  import { KeyNextNodeOperator, KeyProcessStartNextNodeOperator, isCountersign } from './engine.js';
8
+ import { EventType } from './extensions.js';
8
9
  // submitType 枚举(对齐 boot3)
9
10
  const SUBMIT_APPLY = 0;
10
11
  const SUBMIT_AGREE = 1;
@@ -132,6 +133,12 @@ export class JeeflowFacade {
132
133
  return this.taskAddActor(args);
133
134
  case 'processTask/latest':
134
135
  return this.taskLatest(args);
136
+ case 'processInstance/stats/overview':
137
+ return this.statsOverview(args);
138
+ case 'processInstance/stats/trend':
139
+ return this.statsTrend(args);
140
+ case 'processInstance/stats/group':
141
+ return this.statsGroup(args);
135
142
  default:
136
143
  throw new Error(`未知 action: ${action}`);
137
144
  }
@@ -153,6 +160,11 @@ export class JeeflowFacade {
153
160
  ? flowArgs.f_ccActors.split(',').map((x) => x.trim()).filter(Boolean) : [];
154
161
  if (ccList.length > 0) {
155
162
  await this.repo.createCcInstance(inst.id, operator, ...ccList);
163
+ // issues/102:CC 实例落库后逐抄送人 fire CcCreate(ccActorId 直传事件体,
164
+ // 对齐 Go startAndExecute / Python _startAndExecute;监听器据此落抄送知会 NOTICE)
165
+ for (const actor of ccList) {
166
+ await this.engine.fireEvent({ type: EventType.CcCreate, instanceId: inst.id, operator, ccActorId: actor });
167
+ }
156
168
  }
157
169
  // startAndExecute:自动完成申请节点(assignee="applicant" → 发起人)
158
170
  const doing = await this.repo.findDoingTasks(inst.id);
@@ -240,7 +252,8 @@ export class JeeflowFacade {
240
252
  // 撤回:废弃全部 doing 任务 + 实例状态(v1.0.1:updateInstance 级联落库)
241
253
  const operator = String(args.operator ?? 'user1');
242
254
  const now = new Date();
243
- // findInstanceById 不加载 tasks(空),必须按实例查 doing 任务废弃
255
+ // findInstanceById 现水合 tasks(issues/110),此处仍按实例单独查 doing 任务废弃,
256
+ // 且必须把聚合副本重置为仅被废弃项(见下方 inst.tasks = abandoned),防级联回写多余任务
244
257
  const abandoned = [];
245
258
  for (const t of await this.repo.findDoingTasks(instanceId)) {
246
259
  t.abandon(now);
@@ -807,6 +820,10 @@ export class JeeflowFacade {
807
820
  if (actors.length === 0)
808
821
  throw new Error('actorIds 缺失');
809
822
  await this.repo.createCcInstance(instanceId, operator, ...actors);
823
+ // issues/102:手动 CC 与发起路径同语义——逐抄送人 fire CcCreate
824
+ for (const actor of actors) {
825
+ await this.engine.fireEvent({ type: EventType.CcCreate, instanceId, operator, ccActorId: actor });
826
+ }
810
827
  }
811
828
  async updateCCStatus(args) {
812
829
  const instanceId = toId(args.processInstanceId);
@@ -1091,6 +1108,197 @@ export class JeeflowFacade {
1091
1108
  return undefined;
1092
1109
  }
1093
1110
  }
1111
+ // ── 统计(v1.8.25,issues/103) ──────────────────────────────────────────
1112
+ static DEFAULT_STATE_IN = [10, 20, 30, 40, 45, 50];
1113
+ static DEFAULT_STATS_LIMIT = 10;
1114
+ static VALID_GRANULARITY = new Set(['hour', 'day', 'week', 'month']);
1115
+ static VALID_DIMENSION = new Set([
1116
+ 'state', 'define', 'category', 'approver', 'applicant',
1117
+ 'node', 'stuckNode', 'stuckApprover', 'durationBucket',
1118
+ ]);
1119
+ async statsOverview(args) {
1120
+ const start = parseSurrogateTime(args.start);
1121
+ const end = parseSurrogateTime(args.end);
1122
+ const stateIn = args.stateIn?.length ? args.stateIn.map(Number) : JeeflowFacade.DEFAULT_STATE_IN;
1123
+ const insts = await this.repo.queryInstancesForStats(stateIn, start, end);
1124
+ const total = insts.length;
1125
+ const inProgress = insts.filter(r => r.state === 10).length;
1126
+ const completed = insts.filter(r => r.state === 20).length;
1127
+ const withdrawn = insts.filter(r => r.state === 30).length;
1128
+ const rejected = insts.filter(r => r.state === 45).length;
1129
+ const suspended = insts.filter(r => r.state === 50).length;
1130
+ const now = new Date();
1131
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1132
+ const todayEnd = new Date(todayStart.getTime() + 86400000);
1133
+ // E:todayNew 恒按服务器当日、不过滤 state / 不受 stateIn 影响(对齐内置线 countTodayNew)
1134
+ const todayInsts = await this.repo.queryInstancesForStats(null, todayStart, todayEnd);
1135
+ const todayNew = todayInsts.length;
1136
+ const [pending, overdue] = await this.repo.statsPendingAndOverdueCount();
1137
+ const avgDur = await this.repo.statsAvgCompletedDurationSeconds(start, end);
1138
+ const [csTotal, csCount, onTime, onTimeDenom] = await this.repo.statsCompletedTaskAggregate();
1139
+ const countersignRate = csTotal > 0 ? statsRound4(csCount / csTotal) : 0;
1140
+ const onTimeRate = onTimeDenom > 0 ? statsRound4(onTime / onTimeDenom) : 0;
1141
+ const rejectRate = statsRound4(rejected / Math.max(1, completed + rejected));
1142
+ return {
1143
+ total, inProgress, completed, rejected, withdrawn, suspended,
1144
+ todayNew, avgDurationSeconds: avgDur,
1145
+ rejectRate, pendingTaskCount: pending,
1146
+ overdueTaskCount: overdue, countersignRate, onTimeRate,
1147
+ };
1148
+ }
1149
+ async statsTrend(args) {
1150
+ const granularity = String(args.granularity ?? '');
1151
+ if (!JeeflowFacade.VALID_GRANULARITY.has(granularity)) {
1152
+ throw new Error(`不支持的 granularity: ${granularity}`);
1153
+ }
1154
+ const start = parseSurrogateTime(args.start);
1155
+ const end = parseSurrogateTime(args.end);
1156
+ // C:start/end 必填(对齐内置线 20010012 缺参语义),不静默回退不限时间
1157
+ if (!start || !end) {
1158
+ throw new Error('trend 缺少必填参数:start/end/granularity');
1159
+ }
1160
+ // 实例侧无 state 过滤(对齐内置线 countInstanceStartedByBucket)
1161
+ const insts = await this.repo.queryInstancesForStats(null, start, end);
1162
+ const doneTasks = await this.repo.queryTasksForStats(20 /* TaskState.Done */, start, end);
1163
+ const buckets = statsEnumerateBuckets(start, end, granularity);
1164
+ const startedMap = new Map();
1165
+ for (const r of insts) {
1166
+ const ct = parseSurrogateTime(r.createTime);
1167
+ if (ct) {
1168
+ const bk = statsBucketKey(ct, granularity);
1169
+ startedMap.set(bk, (startedMap.get(bk) ?? 0) + 1);
1170
+ }
1171
+ }
1172
+ const finishedMap = new Map();
1173
+ for (const r of doneTasks) {
1174
+ const ft = parseSurrogateTime(r.finishTime);
1175
+ if (ft) {
1176
+ const bk = statsBucketKey(ft, granularity);
1177
+ finishedMap.set(bk, (finishedMap.get(bk) ?? 0) + 1);
1178
+ }
1179
+ }
1180
+ const series = buckets.map(b => ({
1181
+ bucket: b, started: startedMap.get(b) ?? 0, finished: finishedMap.get(b) ?? 0,
1182
+ }));
1183
+ // A:data 本体为裸数组(去掉 {granularity, series} 包装,对齐契约 spec 06 §4.2 / 内置线)
1184
+ return series;
1185
+ }
1186
+ async statsGroup(args) {
1187
+ const dimension = String(args.dimension ?? '');
1188
+ if (!JeeflowFacade.VALID_DIMENSION.has(dimension)) {
1189
+ throw new Error(`不支持的 dimension: ${dimension}`);
1190
+ }
1191
+ const start = parseSurrogateTime(args.start);
1192
+ const end = parseSurrogateTime(args.end);
1193
+ const limit = args.limit ? Number(args.limit) : JeeflowFacade.DEFAULT_STATS_LIMIT;
1194
+ let rows;
1195
+ if (dimension === 'define') {
1196
+ const raw = await this.repo.statsDefineGroup(start, end, limit);
1197
+ rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
1198
+ }
1199
+ else if (dimension === 'state') {
1200
+ // 无 state 过滤(对齐内置线 groupByDimension:仅按时间限定,契约 group 无 stateIn 入参)
1201
+ const insts = await this.repo.queryInstancesForStats(null, start, end);
1202
+ const grouped = new Map();
1203
+ for (const r of insts) {
1204
+ const k = String(r.state);
1205
+ grouped.set(k, (grouped.get(k) ?? 0) + 1);
1206
+ }
1207
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
1208
+ rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
1209
+ }
1210
+ else if (dimension === 'category') {
1211
+ const insts = await this.repo.queryInstancesForStats(null, start, end);
1212
+ const defineTypes = new Map();
1213
+ for (const r of insts) {
1214
+ if (!defineTypes.has(r.defineId)) {
1215
+ const def = await this.repo.findDefineById(r.defineId);
1216
+ defineTypes.set(r.defineId, def?.type ?? '');
1217
+ }
1218
+ }
1219
+ const grouped = new Map();
1220
+ for (const r of insts) {
1221
+ const tp = defineTypes.get(r.defineId) ?? '';
1222
+ grouped.set(tp, (grouped.get(tp) ?? 0) + 1);
1223
+ }
1224
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
1225
+ rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
1226
+ }
1227
+ else if (dimension === 'approver') {
1228
+ const tasks = await this.repo.queryTasksForStats(20, start, end);
1229
+ const grouped = new Map();
1230
+ for (const r of tasks) {
1231
+ if (!r.operator)
1232
+ continue;
1233
+ grouped.set(r.operator, (grouped.get(r.operator) ?? 0) + 1);
1234
+ }
1235
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
1236
+ rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
1237
+ }
1238
+ else if (dimension === 'applicant') {
1239
+ const insts = await this.repo.queryInstancesForStats(null, start, end);
1240
+ const grouped = new Map();
1241
+ for (const r of insts) {
1242
+ if (!r.operator)
1243
+ continue;
1244
+ grouped.set(r.operator, (grouped.get(r.operator) ?? 0) + 1);
1245
+ }
1246
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
1247
+ rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
1248
+ }
1249
+ else if (dimension === 'node') {
1250
+ const tasks = await this.repo.queryTasksForStats(20, start, end);
1251
+ const nodeAgg = new Map();
1252
+ for (const r of tasks) {
1253
+ if (!r.displayName)
1254
+ continue;
1255
+ let dur = 0;
1256
+ const ft = parseSurrogateTime(r.finishTime);
1257
+ const ct = parseSurrogateTime(r.createTime);
1258
+ if (ft && ct)
1259
+ dur = Math.floor((ft.getTime() - ct.getTime()) / 1000);
1260
+ const agg = nodeAgg.get(r.displayName) ?? { count: 0, totalDur: 0 };
1261
+ agg.count++;
1262
+ agg.totalDur += dur;
1263
+ nodeAgg.set(r.displayName, agg);
1264
+ }
1265
+ const entries = [...nodeAgg.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, limit);
1266
+ rows = entries.map(([name, agg]) => ({
1267
+ key: name, label: null, count: agg.count,
1268
+ avgDurationSeconds: agg.count > 0 ? Math.round(agg.totalDur / agg.count) : null,
1269
+ }));
1270
+ }
1271
+ else if (dimension === 'stuckNode') {
1272
+ const raw = await this.repo.statsStuckNodeGroup(limit);
1273
+ rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
1274
+ }
1275
+ else if (dimension === 'stuckApprover') {
1276
+ const raw = await this.repo.statsStuckApproverGroup(limit);
1277
+ rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
1278
+ }
1279
+ else if (dimension === 'durationBucket') {
1280
+ const durations = await this.repo.statsCompletedInstanceDurations(start, end);
1281
+ let sameDay = 0, d1to3 = 0, d3to7 = 0, over7d = 0;
1282
+ for (const dur of durations) {
1283
+ if (dur < 86400)
1284
+ sameDay++;
1285
+ else if (dur < 259200)
1286
+ d1to3++;
1287
+ else if (dur < 604800)
1288
+ d3to7++;
1289
+ else
1290
+ over7d++;
1291
+ }
1292
+ const keys = ['sameDay', '1to3d', '3to7d', 'over7d'];
1293
+ const counts = [sameDay, d1to3, d3to7, over7d];
1294
+ rows = keys.map((k, i) => ({ key: k, label: null, count: counts[i], avgDurationSeconds: null }));
1295
+ }
1296
+ else {
1297
+ rows = [];
1298
+ }
1299
+ // A:data 本体为裸数组(去掉 {dimension, rows} 包装,对齐契约 spec 06 §4.2 / 内置线)
1300
+ return rows;
1301
+ }
1094
1302
  }
1095
1303
  // ── 行转 Map(issues/05-2 列表字段契约 + 05-3 时间格式)─────────────────────
1096
1304
  /** issues/15:取 vars 中 prefix 前缀字段,输出「带前缀 + 去前缀副本」(对齐 boot3 getFormData) */
@@ -1304,3 +1512,82 @@ function pageData(pageNum, pageSize, total, rows) {
1304
1512
  totalPage = Math.ceil(total / ps);
1305
1513
  return { pageNum: pn, pageSize: ps, recordCount: total, totalPage, rows };
1306
1514
  }
1515
+ // ═══ 统计辅助(v1.8.25,issues/103) ═══
1516
+ function statsRound4(v) {
1517
+ return Math.round(v * 10000) / 10000;
1518
+ }
1519
+ function statsWeekKey(d) {
1520
+ const iso = isoWeek(d);
1521
+ return `${iso[0]}-W${String(iso[1]).padStart(2, '0')}`;
1522
+ }
1523
+ function isoWeek(d) {
1524
+ const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
1525
+ const dayNum = date.getUTCDay() || 7;
1526
+ date.setUTCDate(date.getUTCDate() + 4 - dayNum);
1527
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
1528
+ const weekNo = Math.ceil((((date.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
1529
+ return [date.getUTCFullYear(), weekNo];
1530
+ }
1531
+ function statsEnumerateBuckets(start, end, granularity) {
1532
+ const now = new Date();
1533
+ const s = start ?? new Date(now.getTime() - 30 * 86400000);
1534
+ const e = end ?? now;
1535
+ const buckets = [];
1536
+ if (granularity === 'hour') {
1537
+ const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate(), s.getHours(), 0, 0);
1538
+ while (cursor <= e) {
1539
+ buckets.push(formatHour(cursor));
1540
+ cursor.setHours(cursor.getHours() + 1);
1541
+ }
1542
+ }
1543
+ else if (granularity === 'day') {
1544
+ const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate());
1545
+ const endDay = new Date(e.getFullYear(), e.getMonth(), e.getDate());
1546
+ while (cursor <= endDay) {
1547
+ buckets.push(formatDay(cursor));
1548
+ cursor.setDate(cursor.getDate() + 1);
1549
+ }
1550
+ }
1551
+ else if (granularity === 'week') {
1552
+ const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate());
1553
+ const weekday = cursor.getDay() || 7;
1554
+ cursor.setDate(cursor.getDate() - (weekday - 1));
1555
+ const endDay = new Date(e.getFullYear(), e.getMonth(), e.getDate());
1556
+ while (cursor <= endDay) {
1557
+ buckets.push(statsWeekKey(cursor));
1558
+ cursor.setDate(cursor.getDate() + 7);
1559
+ }
1560
+ }
1561
+ else if (granularity === 'month') {
1562
+ let year = s.getFullYear();
1563
+ let month = s.getMonth();
1564
+ const endYear = e.getFullYear();
1565
+ const endMonth = e.getMonth();
1566
+ while (year < endYear || (year === endYear && month <= endMonth)) {
1567
+ buckets.push(`${year}-${String(month + 1).padStart(2, '0')}`);
1568
+ month++;
1569
+ if (month > 11) {
1570
+ month = 0;
1571
+ year++;
1572
+ }
1573
+ }
1574
+ }
1575
+ return buckets;
1576
+ }
1577
+ function statsBucketKey(d, granularity) {
1578
+ if (granularity === 'hour')
1579
+ return formatHour(d);
1580
+ if (granularity === 'day')
1581
+ return formatDay(d);
1582
+ if (granularity === 'week')
1583
+ return statsWeekKey(d);
1584
+ if (granularity === 'month')
1585
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
1586
+ return '';
1587
+ }
1588
+ function formatHour(d) {
1589
+ return `${formatDay(d)} ${String(d.getHours()).padStart(2, '0')}:00`;
1590
+ }
1591
+ function formatDay(d) {
1592
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
1593
+ }
@@ -1,5 +1,5 @@
1
1
  import { ProcessInstance, ProcessTask, type ProcessDefine, type CcInstanceRow, type DefineRow, type InstanceRow, type TaskRow } from '../model.js';
2
- import type { IDGenerator, ProcessRepository, QueryCondition } from '../spi.js';
2
+ import type { IDGenerator, ProcessRepository, QueryCondition, InstanceStatsRow, TaskStatsRow } from '../spi.js';
3
3
  /** 默认 ID 生成器:时间戳毫秒 + 同毫秒递增序号(对齐 Java nextId 默认实现)——
4
4
  * issue 38 E9:返回 string(数字值在 2^53 内精确,转字符串统一承载) */
5
5
  export declare class TsIDGenerator implements IDGenerator {
@@ -95,4 +95,13 @@ export declare class JdbcRepository implements ProcessRepository {
95
95
  total: number;
96
96
  }>;
97
97
  private mapCcRow;
98
+ queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<InstanceStatsRow[]>;
99
+ queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<TaskStatsRow[]>;
100
+ statsPendingAndOverdueCount(): Promise<[number, number]>;
101
+ statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
102
+ statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
103
+ statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<Record<string, any>[]>;
104
+ statsStuckNodeGroup(limit?: number): Promise<Record<string, any>[]>;
105
+ statsStuckApproverGroup(limit?: number): Promise<Record<string, any>[]>;
106
+ statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
98
107
  }
@@ -213,7 +213,11 @@ export class JdbcRepository {
213
213
  operator: row.operator, expireTime: row.expire_time,
214
214
  createTime: row.create_time, createUser: rowId(row.create_user),
215
215
  updateTime: row.update_time, updateUser: rowId(row.update_user),
216
- tasks: [],
216
+ // issues/110:聚合水合——二次查 wf_process_task 装任务副本(含 actorIds),
217
+ // 对齐 Java findTasksByInstanceId / PHP PdoProcessRepository / C# issues/89;
218
+ // 否则门面 detail 的 tasks/activeTaskList 恒空。
219
+ // 复用 findHistoryTasks(ORDER BY id ASC + 批查 actor,事务内经 c() 复用连接)
220
+ tasks: await this.findHistoryTasks(rowId(row.id)),
217
221
  });
218
222
  inst.variables = row.variable ? JSON.parse(row.variable) : {};
219
223
  return inst;
@@ -648,4 +652,208 @@ export class JdbcRepository {
648
652
  defineVersion: Number(r.version ?? 0),
649
653
  };
650
654
  }
655
+ // ── Stats(issues/103:统计接口契约) ─────────────────────────────────────
656
+ async queryInstancesForStats(stateIn, start, end) {
657
+ // stateIn 空 = 无 state 过滤(对齐内置线:仅 overview 六计数用 stateIn)
658
+ let sql = 'SELECT process_define_id, state, operator, create_time FROM wf_process_instance WHERE 1=1';
659
+ const args = [];
660
+ if (stateIn && stateIn.length) {
661
+ sql += ` AND state IN (${repeatPh(stateIn.length)})`;
662
+ args.push(...stateIn);
663
+ }
664
+ if (start) {
665
+ sql += ' AND create_time >= ?';
666
+ args.push(start);
667
+ }
668
+ if (end) {
669
+ sql += ' AND create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
670
+ args.push(end);
671
+ }
672
+ sql += ' ORDER BY create_time';
673
+ const conn = await this.c();
674
+ try {
675
+ const rows = await conn.fetchAll(this.sql(sql), args);
676
+ return rows.map((r) => ({
677
+ defineId: rowId(r.process_define_id), state: r.state,
678
+ operator: r.operator ?? '', createTime: r.create_time,
679
+ }));
680
+ }
681
+ finally {
682
+ await this.done(conn);
683
+ }
684
+ }
685
+ async queryTasksForStats(taskState, start, end) {
686
+ let sql = 'SELECT operator, display_name, perform_type, create_time, finish_time, expire_time FROM wf_process_task WHERE 1=1';
687
+ const args = [];
688
+ if (taskState != null) {
689
+ sql += ' AND task_state = ?';
690
+ args.push(taskState);
691
+ }
692
+ if (start) {
693
+ sql += ' AND finish_time >= ?';
694
+ args.push(start);
695
+ }
696
+ if (end) {
697
+ sql += ' AND finish_time < DATE_ADD(?, INTERVAL 1 SECOND)';
698
+ args.push(end);
699
+ }
700
+ const conn = await this.c();
701
+ try {
702
+ const rows = await conn.fetchAll(this.sql(sql), args);
703
+ return rows.map((r) => ({
704
+ operator: r.operator ?? '', displayName: r.display_name ?? '',
705
+ performType: r.perform_type ?? 0, createTime: r.create_time,
706
+ finishTime: r.finish_time, expireTime: r.expire_time,
707
+ }));
708
+ }
709
+ finally {
710
+ await this.done(conn);
711
+ }
712
+ }
713
+ async statsPendingAndOverdueCount() {
714
+ const conn = await this.c();
715
+ try {
716
+ const r1 = await conn.fetchOne(this.sql('SELECT COUNT(*) FROM wf_process_task WHERE task_state = 10'), []);
717
+ const pending = r1 ? Number(Object.values(r1)[0] ?? 0) : 0;
718
+ const now = new Date();
719
+ const r2 = await conn.fetchOne(this.sql('SELECT COUNT(*) FROM wf_process_task WHERE task_state = 10 AND expire_time IS NOT NULL AND expire_time < ?'), [now]);
720
+ const overdue = r2 ? Number(Object.values(r2)[0] ?? 0) : 0;
721
+ return [pending, overdue];
722
+ }
723
+ finally {
724
+ await this.done(conn);
725
+ }
726
+ }
727
+ async statsCompletedTaskAggregate() {
728
+ const conn = await this.c();
729
+ try {
730
+ const r = await conn.fetchOne(this.sql('SELECT COUNT(*) AS total, ' +
731
+ 'SUM(CASE WHEN perform_type = 1 THEN 1 ELSE 0 END) AS countersign, ' +
732
+ 'SUM(CASE WHEN expire_time IS NOT NULL AND finish_time IS NOT NULL AND finish_time <= expire_time THEN 1 ELSE 0 END) AS on_time, ' +
733
+ 'SUM(CASE WHEN expire_time IS NOT NULL THEN 1 ELSE 0 END) AS on_time_denom ' +
734
+ 'FROM wf_process_task WHERE task_state = 20'), []);
735
+ if (!r)
736
+ return [0, 0, 0, 0];
737
+ return [
738
+ Number(r.total ?? 0),
739
+ Number(r.countersign ?? 0),
740
+ Number(r.on_time ?? 0),
741
+ Number(r.on_time_denom ?? 0),
742
+ ];
743
+ }
744
+ finally {
745
+ await this.done(conn);
746
+ }
747
+ }
748
+ async statsAvgCompletedDurationSeconds(start, end) {
749
+ let sql = `SELECT COALESCE(ROUND(AVG(
750
+ TIMESTAMPDIFF(SECOND, i.create_time, (
751
+ SELECT MAX(t.finish_time) FROM wf_process_task t WHERE t.process_instance_id = i.id AND t.finish_time IS NOT NULL
752
+ ))
753
+ )), 0) AS avg_sec FROM wf_process_instance i WHERE i.state = 20`;
754
+ const args = [];
755
+ if (start) {
756
+ sql += ' AND i.create_time >= ?';
757
+ args.push(start);
758
+ }
759
+ if (end) {
760
+ sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
761
+ args.push(end);
762
+ }
763
+ const conn = await this.c();
764
+ try {
765
+ const r = await conn.fetchOne(this.sql(sql), args);
766
+ if (!r)
767
+ return 0;
768
+ return Number(Object.values(r)[0] ?? 0);
769
+ }
770
+ finally {
771
+ await this.done(conn);
772
+ }
773
+ }
774
+ async statsDefineGroup(start, end, limit = 10) {
775
+ // 对齐内置线 mapper:count 全实例(无 state 过滤)、inner join define、
776
+ // avg 仅对 state=20 且有 finish 的实例聚合(MAX(task.finish_time) - create_time)
777
+ let sql = 'SELECT pd.name, pd.display_name, COUNT(*) AS cnt, ' +
778
+ 'ROUND(AVG(CASE WHEN i.state = 20 AND sub.maxft IS NOT NULL ' +
779
+ 'THEN TIMESTAMPDIFF(SECOND, i.create_time, sub.maxft) END)) AS avg_dur ' +
780
+ 'FROM wf_process_instance i ' +
781
+ 'JOIN wf_process_define pd ON i.process_define_id = pd.id ' +
782
+ 'LEFT JOIN (SELECT process_instance_id, MAX(finish_time) AS maxft ' +
783
+ 'FROM wf_process_task GROUP BY process_instance_id) sub ' +
784
+ 'ON sub.process_instance_id = i.id ' +
785
+ 'WHERE 1=1';
786
+ const args = [];
787
+ if (start) {
788
+ sql += ' AND i.create_time >= ?';
789
+ args.push(start);
790
+ }
791
+ if (end) {
792
+ sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
793
+ args.push(end);
794
+ }
795
+ sql += ' GROUP BY pd.id, pd.name, pd.display_name ORDER BY cnt DESC LIMIT ?';
796
+ args.push(limit);
797
+ const conn = await this.c();
798
+ try {
799
+ const rows = await conn.fetchAll(this.sql(sql), args);
800
+ return rows.map((r) => ({
801
+ key: r.name ?? null, label: r.display_name ?? null,
802
+ count: Number(r.cnt ?? 0),
803
+ avgDurationSeconds: r.avg_dur != null ? Math.round(Number(r.avg_dur)) : null,
804
+ }));
805
+ }
806
+ finally {
807
+ await this.done(conn);
808
+ }
809
+ }
810
+ async statsStuckNodeGroup(limit = 10) {
811
+ const conn = await this.c();
812
+ try {
813
+ const rows = await conn.fetchAll(this.sql('SELECT display_name, COUNT(*) AS cnt FROM wf_process_task ' +
814
+ 'WHERE task_state = 10 GROUP BY display_name ORDER BY cnt DESC LIMIT ?'), [limit]);
815
+ return rows.map((r) => ({
816
+ key: r.display_name ?? '', label: null, count: Number(r.cnt ?? 0), avgDurationSeconds: null,
817
+ }));
818
+ }
819
+ finally {
820
+ await this.done(conn);
821
+ }
822
+ }
823
+ async statsStuckApproverGroup(limit = 10) {
824
+ const conn = await this.c();
825
+ try {
826
+ const rows = await conn.fetchAll(this.sql('SELECT ta.actor_id, COUNT(DISTINCT t.id) AS cnt FROM wf_process_task_actor ta ' +
827
+ 'INNER JOIN wf_process_task t ON ta.process_task_id = t.id ' +
828
+ 'WHERE t.task_state = 10 GROUP BY ta.actor_id ORDER BY cnt DESC LIMIT ?'), [limit]);
829
+ return rows.map((r) => ({
830
+ key: rowId(r.actor_id), label: null, count: Number(r.cnt ?? 0), avgDurationSeconds: null,
831
+ }));
832
+ }
833
+ finally {
834
+ await this.done(conn);
835
+ }
836
+ }
837
+ async statsCompletedInstanceDurations(start, end) {
838
+ let sql = `SELECT TIMESTAMPDIFF(SECOND, i.create_time, (
839
+ SELECT MAX(t.finish_time) FROM wf_process_task t WHERE t.process_instance_id = i.id AND t.finish_time IS NOT NULL
840
+ )) AS dur FROM wf_process_instance i WHERE i.state = 20`;
841
+ const args = [];
842
+ if (start) {
843
+ sql += ' AND i.create_time >= ?';
844
+ args.push(start);
845
+ }
846
+ if (end) {
847
+ sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
848
+ args.push(end);
849
+ }
850
+ const conn = await this.c();
851
+ try {
852
+ const rows = await conn.fetchAll(this.sql(sql), args);
853
+ return rows.map((r) => Number(r.dur)).filter((v) => v != null && !isNaN(v));
854
+ }
855
+ finally {
856
+ await this.done(conn);
857
+ }
858
+ }
651
859
  }
package/dist/memory.d.ts CHANGED
@@ -57,4 +57,37 @@ export declare class MemoryRepository implements ProcessRepository {
57
57
  allDefines(): ProcessDefine[];
58
58
  allInstances(): ProcessInstance[];
59
59
  allTasks(): ProcessTask[];
60
+ private toDt;
61
+ queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<{
62
+ defineId: string;
63
+ state: number;
64
+ operator: string;
65
+ createTime: Date | string | null;
66
+ }[]>;
67
+ queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<{
68
+ operator: string;
69
+ displayName: string;
70
+ performType: number;
71
+ createTime: Date | string | null;
72
+ finishTime: Date | string | null;
73
+ expireTime: Date | string | null;
74
+ }[]>;
75
+ statsPendingAndOverdueCount(): Promise<[number, number]>;
76
+ statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
77
+ statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
78
+ statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<{
79
+ key: string;
80
+ label: string | null;
81
+ count: number;
82
+ avgDurationSeconds: number | null;
83
+ }[]>;
84
+ statsStuckNodeGroup(limit?: number): Promise<{
85
+ key: string;
86
+ count: number;
87
+ }[]>;
88
+ statsStuckApproverGroup(limit?: number): Promise<{
89
+ key: string;
90
+ count: number;
91
+ }[]>;
92
+ statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
60
93
  }
package/dist/memory.js CHANGED
@@ -144,13 +144,13 @@ export class MemoryRepository {
144
144
  cp.tasks = [];
145
145
  this.instances.set(inst.id, cp);
146
146
  // v1.0.1:级联保存聚合根内任务状态变更
147
- for (const t of inst.tasks) {
147
+ for (const t of (inst.tasks ?? [])) {
148
148
  if (!t.id)
149
149
  continue;
150
150
  const tc = cloneTask(t);
151
151
  tc.actorIds = [];
152
152
  this.tasks.set(t.id, tc);
153
- if (t.actorIds.length)
153
+ if (t.actorIds?.length)
154
154
  this.actors.set(t.id, [...t.actorIds]);
155
155
  }
156
156
  }
@@ -183,14 +183,14 @@ export class MemoryRepository {
183
183
  const cp = cloneTask(task);
184
184
  cp.actorIds = [];
185
185
  this.tasks.set(task.id, cp);
186
- if (task.actorIds.length)
186
+ if (task.actorIds?.length)
187
187
  this.actors.set(task.id, [...task.actorIds]);
188
188
  }
189
189
  async updateTask(task) {
190
190
  const cp = cloneTask(task);
191
191
  cp.actorIds = [];
192
192
  this.tasks.set(task.id, cp);
193
- if (task.actorIds.length)
193
+ if (task.actorIds?.length)
194
194
  this.actors.set(task.id, [...task.actorIds]);
195
195
  }
196
196
  async findDoingTasks(instanceId, taskNames) {
@@ -375,4 +375,198 @@ export class MemoryRepository {
375
375
  return cp;
376
376
  });
377
377
  }
378
+ // ── 统计(v1.8.25,issues/103) ──────────────────────────────────────────
379
+ toDt(v) {
380
+ if (v == null)
381
+ return undefined;
382
+ if (v instanceof Date)
383
+ return isNaN(v.getTime()) ? undefined : v;
384
+ const d = new Date(String(v).replace(' ', 'T'));
385
+ return isNaN(d.getTime()) ? undefined : d;
386
+ }
387
+ async queryInstancesForStats(stateIn, start, end) {
388
+ const sd = this.toDt(start);
389
+ const ed = this.toDt(end);
390
+ const rows = [];
391
+ for (const inst of this.instances.values()) {
392
+ const sv = Number(inst.state);
393
+ // stateIn 空 = 无 state 过滤(对齐内置线:仅 overview 六计数用 stateIn)
394
+ if (stateIn && stateIn.length && !stateIn.includes(sv))
395
+ continue;
396
+ const ct = this.toDt(inst.createTime);
397
+ if (sd && ct && ct < sd)
398
+ continue;
399
+ if (ed && ct && ct > ed)
400
+ continue;
401
+ rows.push({ defineId: String(inst.defineId), state: sv, operator: inst.operator ?? '', createTime: inst.createTime ?? null });
402
+ }
403
+ return rows;
404
+ }
405
+ async queryTasksForStats(taskState, start, end) {
406
+ const sd = this.toDt(start);
407
+ const ed = this.toDt(end);
408
+ const rows = [];
409
+ for (const t of this.tasks.values()) {
410
+ if (taskState != null && Number(t.taskState) !== taskState)
411
+ continue;
412
+ const ft = this.toDt(t.finishTime);
413
+ if (sd && ft && ft < sd)
414
+ continue;
415
+ if (ed && ft && ft > ed)
416
+ continue;
417
+ rows.push({
418
+ operator: t.actorId ?? '', displayName: t.displayName ?? '',
419
+ performType: t.performType ?? 0,
420
+ createTime: t.createTime ?? null, finishTime: t.finishTime ?? null, expireTime: t.expireTime ?? null,
421
+ });
422
+ }
423
+ return rows;
424
+ }
425
+ async statsPendingAndOverdueCount() {
426
+ const now = new Date();
427
+ let pending = 0, overdue = 0;
428
+ for (const t of this.tasks.values()) {
429
+ if (Number(t.taskState) !== TaskState.Doing)
430
+ continue;
431
+ pending++;
432
+ const exp = this.toDt(t.expireTime);
433
+ if (exp && exp < now)
434
+ overdue++;
435
+ }
436
+ return [pending, overdue];
437
+ }
438
+ async statsCompletedTaskAggregate() {
439
+ let total = 0, countersign = 0, onTime = 0, onTimeDenom = 0;
440
+ for (const t of this.tasks.values()) {
441
+ if (Number(t.taskState) !== TaskState.Done)
442
+ continue;
443
+ total++;
444
+ if (t.performType === 1)
445
+ countersign++;
446
+ const ft = this.toDt(t.finishTime);
447
+ const exp = this.toDt(t.expireTime);
448
+ if (exp != null) {
449
+ onTimeDenom++;
450
+ if (ft && ft <= exp)
451
+ onTime++;
452
+ }
453
+ }
454
+ return [total, countersign, onTime, onTimeDenom];
455
+ }
456
+ async statsAvgCompletedDurationSeconds(start, end) {
457
+ const sd = this.toDt(start);
458
+ const ed = this.toDt(end);
459
+ let totalSec = 0, count = 0;
460
+ for (const inst of this.instances.values()) {
461
+ if (Number(inst.state) !== 20)
462
+ continue;
463
+ const ct = this.toDt(inst.createTime);
464
+ if (sd && ct && ct < sd)
465
+ continue;
466
+ if (ed && ct && ct > ed)
467
+ continue;
468
+ let maxFt;
469
+ for (const t of this.tasks.values()) {
470
+ if (t.processInstanceId !== inst.id)
471
+ continue;
472
+ const ft = this.toDt(t.finishTime);
473
+ if (ft && (!maxFt || ft > maxFt))
474
+ maxFt = ft;
475
+ }
476
+ if (maxFt && ct) {
477
+ totalSec += Math.floor((maxFt.getTime() - ct.getTime()) / 1000);
478
+ count++;
479
+ }
480
+ }
481
+ return count > 0 ? Math.floor(totalSec / count) : 0;
482
+ }
483
+ async statsDefineGroup(start, end, limit = 10) {
484
+ const sd = this.toDt(start);
485
+ const ed = this.toDt(end);
486
+ const grouped = new Map();
487
+ for (const inst of this.instances.values()) {
488
+ const ct = this.toDt(inst.createTime);
489
+ if (sd && ct && ct < sd)
490
+ continue;
491
+ if (ed && ct && ct > ed)
492
+ continue;
493
+ const did = String(inst.defineId);
494
+ if (!grouped.has(did)) {
495
+ const defn = this.defines.get(did);
496
+ grouped.set(did, { key: defn?.name ?? '', label: defn?.displayName ?? null, count: 0, totalDur: 0, durCount: 0 });
497
+ }
498
+ const g = grouped.get(did);
499
+ g.count++;
500
+ if (Number(inst.state) === 20) {
501
+ let maxFt;
502
+ for (const t of this.tasks.values()) {
503
+ if (t.processInstanceId !== inst.id)
504
+ continue;
505
+ const ft = this.toDt(t.finishTime);
506
+ if (ft && (!maxFt || ft > maxFt))
507
+ maxFt = ft;
508
+ }
509
+ if (maxFt && ct) {
510
+ g.totalDur += Math.floor((maxFt.getTime() - ct.getTime()) / 1000);
511
+ g.durCount++;
512
+ }
513
+ }
514
+ }
515
+ const entries = [...grouped.values()].sort((a, b) => b.count - a.count).slice(0, limit);
516
+ return entries.map(e => ({ key: e.key, label: e.label, count: e.count, avgDurationSeconds: e.durCount > 0 ? Math.floor(e.totalDur / e.durCount) : null }));
517
+ }
518
+ async statsStuckNodeGroup(limit = 10) {
519
+ const grouped = new Map();
520
+ for (const t of this.tasks.values()) {
521
+ if (Number(t.taskState) !== TaskState.Doing)
522
+ continue;
523
+ const dn = t.displayName;
524
+ if (!dn)
525
+ continue;
526
+ grouped.set(dn, (grouped.get(dn) ?? 0) + 1);
527
+ }
528
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
529
+ return entries.map(([k, c]) => ({ key: k, count: c }));
530
+ }
531
+ async statsStuckApproverGroup(limit = 10) {
532
+ const grouped = new Map();
533
+ for (const t of this.tasks.values()) {
534
+ if (Number(t.taskState) !== TaskState.Doing)
535
+ continue;
536
+ const actors = this.actors.get(t.id) ?? [];
537
+ for (const aid of actors) {
538
+ if (!aid)
539
+ continue;
540
+ grouped.set(aid, (grouped.get(aid) ?? 0) + 1);
541
+ }
542
+ }
543
+ const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
544
+ return entries.map(([k, c]) => ({ key: k, count: c }));
545
+ }
546
+ async statsCompletedInstanceDurations(start, end) {
547
+ const sd = this.toDt(start);
548
+ const ed = this.toDt(end);
549
+ const durations = [];
550
+ for (const inst of this.instances.values()) {
551
+ if (Number(inst.state) !== 20)
552
+ continue;
553
+ const ct = this.toDt(inst.createTime);
554
+ if (sd && ct && ct < sd)
555
+ continue;
556
+ if (ed && ct && ct > ed)
557
+ continue;
558
+ let maxFt;
559
+ for (const t of this.tasks.values()) {
560
+ if (t.processInstanceId !== inst.id)
561
+ continue;
562
+ const ft = this.toDt(t.finishTime);
563
+ if (ft && (!maxFt || ft > maxFt))
564
+ maxFt = ft;
565
+ }
566
+ if (maxFt && ct) {
567
+ durations.push(Math.floor((maxFt.getTime() - ct.getTime()) / 1000));
568
+ }
569
+ }
570
+ return durations;
571
+ }
378
572
  }
package/dist/model.js CHANGED
@@ -212,7 +212,7 @@ export class ProcessTask {
212
212
  // ─── Clone Helpers(保留 class 原型)────────────────────────────────────────────
213
213
  export function cloneInstance(inst) {
214
214
  return Object.assign(Object.create(ProcessInstance.prototype), inst, {
215
- tasks: inst.tasks.map(cloneTask),
215
+ tasks: (inst.tasks ?? []).map(cloneTask),
216
216
  });
217
217
  }
218
218
  export function cloneTask(task) {
package/dist/spi.d.ts CHANGED
@@ -4,6 +4,20 @@ export interface QueryCondition {
4
4
  operator: string;
5
5
  value: any;
6
6
  }
7
+ export interface InstanceStatsRow {
8
+ defineId: string;
9
+ state: number;
10
+ operator: string;
11
+ createTime: Date | string | null;
12
+ }
13
+ export interface TaskStatsRow {
14
+ operator: string;
15
+ displayName: string;
16
+ performType: number;
17
+ createTime: Date | string | null;
18
+ finishTime: Date | string | null;
19
+ expireTime: Date | string | null;
20
+ }
7
21
  export interface ProcessRepository {
8
22
  findDefineById(id: string): Promise<ProcessDefine | null>;
9
23
  findDefineByName(name: string): Promise<ProcessDefine | null>;
@@ -45,6 +59,15 @@ export interface ProcessRepository {
45
59
  rows: TaskRow[];
46
60
  total: number;
47
61
  }>;
62
+ queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<InstanceStatsRow[]>;
63
+ queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<TaskStatsRow[]>;
64
+ statsPendingAndOverdueCount(): Promise<[number, number]>;
65
+ statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
66
+ statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
67
+ statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<Record<string, any>[]>;
68
+ statsStuckNodeGroup(limit?: number): Promise<Record<string, any>[]>;
69
+ statsStuckApproverGroup(limit?: number): Promise<Record<string, any>[]>;
70
+ statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
48
71
  }
49
72
  export interface UserProvider {
50
73
  getUser(userId: string): Promise<UserInfo | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.8.23",
3
+ "version": "1.8.26",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",