@mldong/jeeflow 1.8.17 → 1.8.19

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/builtin.d.ts CHANGED
@@ -12,9 +12,10 @@ export declare const HANDLER_TASK_ROLE_ASSIGNEE: string;
12
12
  export declare class OperatorAssignmentHandler implements IAssignmentHandler {
13
13
  assign(_node: FlowNode, inst: ProcessInstance | null, _operator: string): Promise<string[]>;
14
14
  }
15
- /** 按表单字段值分配参与者:精确匹配 node.idvars 字段;_数字 后缀去后缀再匹配。 */
15
+ /** 按表单字段值分配参与者:f_ 前缀优先裸名回落 _数字 后缀去后缀再匹配。 */
16
16
  export declare class FormFieldAssigneeHandler implements IAssignmentHandler {
17
17
  assign(node: FlowNode, inst: ProcessInstance | null, _operator: string): Promise<string[]>;
18
+ /** issues/48:f_ 前缀优先 → 裸名回落 → 编号后缀去后缀匹配裸名 */
18
19
  private findFieldValue;
19
20
  private collect;
20
21
  private add;
package/dist/builtin.js CHANGED
@@ -21,7 +21,7 @@ export class OperatorAssignmentHandler {
21
21
  return ['apply.operator'];
22
22
  }
23
23
  }
24
- /** 按表单字段值分配参与者:精确匹配 node.idvars 字段;_数字 后缀去后缀再匹配。 */
24
+ /** 按表单字段值分配参与者:f_ 前缀优先裸名回落 _数字 后缀去后缀再匹配。 */
25
25
  export class FormFieldAssigneeHandler {
26
26
  async assign(node, inst, _operator) {
27
27
  if (!inst || !node)
@@ -31,7 +31,10 @@ export class FormFieldAssigneeHandler {
31
31
  return [];
32
32
  return this.collect(value);
33
33
  }
34
+ /** issues/48:f_ 前缀优先 → 裸名回落 → 编号后缀去后缀匹配裸名 */
34
35
  findFieldValue(variables, fieldName) {
36
+ if ('f_' + fieldName in variables)
37
+ return variables['f_' + fieldName];
35
38
  if (fieldName in variables)
36
39
  return variables[fieldName];
37
40
  const m = NUMBER_SUFFIX_PATTERN.exec(fieldName);
package/dist/engine.d.ts CHANGED
@@ -49,6 +49,11 @@ export declare class EngineImpl implements Engine {
49
49
  executeAndJumpToEnd(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
50
50
  executeAndJumpTask(taskId: string, operator: string, args?: Record<string, any>, targetTaskName?: string): Promise<ProcessInstance>;
51
51
  executeAndJumpToFirstTaskNode(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
52
+ private prepareExecuteTask;
53
+ private previousTaskName;
54
+ private isFirstTaskNode;
55
+ private rollbackActors;
56
+ private createTaskWithActors;
52
57
  private loadAndCheck;
53
58
  private executeNode;
54
59
  private evaluateDecision;
package/dist/engine.js CHANGED
@@ -127,31 +127,18 @@ export class EngineImpl {
127
127
  }
128
128
  // ─── Execute ───────────────────────────────────────────────────────────────
129
129
  async executeProcessTask(taskId, operator, args = {}) {
130
- const { task, inst } = await this.loadAndCheck(taskId, operator);
131
- // issues/26:办理提交的 f_ 字段按任务节点字段权限过滤(只读/隐藏不入变量)——
132
- // 被拒值无法经流程变量落到下游节点写入,上游只读声明不可被绕过
133
- const def = await this.repo.findDefineById(inst.defineId);
134
- const flow = JSON.parse(typeof def.content === 'string' ? def.content : new TextDecoder().decode(def.content));
135
- args = filterFieldByPerm(args, findNode(flow, task.taskName));
136
- const vars = { ...inst.variables, ...task.variables, ...args };
137
- await this.addUserInfo(operator, vars);
130
+ const { task, inst, flow, vars } = await this.prepareExecuteTask(taskId, operator, args);
138
131
  const now = new Date();
139
- // 聚合根:完成任务(子实体状态转换 + 实例变量合并)
140
- inst.completeTask(task, operator, vars, now);
141
- await this.repo.updateTask(task);
142
- // v1.0.1:updateInstance 级联持久化依赖聚合内任务副本为最新状态,
143
- // completeTask 改的是外部任务对象,需同步回聚合根
144
- syncTaskToAggregate(inst, task);
145
- await this.fireEvent({ type: EventType.TaskComplete, instanceId: inst.id, taskId: task.id, nodeId: task.taskName, operator });
146
- inst.variables = vars;
147
- await this.repo.updateInstance(inst);
148
132
  const curNode = findNode(flow, task.taskName);
149
133
  if (curNode) {
150
134
  // 1.8.0:任务完成节点自身的后置拦截器(SYNC 同步演进——任务节点推进更新状态/字段)。
151
135
  // createTask 不再触发(引擎语义修正),此处为完成任务节点的唯一触发点
152
136
  await this.firePost(curNode, inst);
153
137
  const ct = curNode.properties?.countersignType;
154
- if (ct === 'SEQUENTIAL') {
138
+ // issues/79:会签一票否决(对齐 Java CountersignHandler / PHP setMerged(true))——
139
+ // submitType=20 COUNTERSIGN_DISAGREE 时跳过会签"未完成即停留"门控,提前流转后续节点
140
+ const csVeto = !!ct && Number(vars[KeySubmitType]) === Number(SubmitType.CountersignDisagree);
141
+ if (ct === 'SEQUENTIAL' && !csVeto) {
155
142
  const doing = await this.repo.findDoingTasks(inst.id);
156
143
  if (doing.length === 0) {
157
144
  const [actors, lc] = getCsState(vars, curNode.id);
@@ -171,7 +158,7 @@ export class EngineImpl {
171
158
  return (await this.repo.findInstanceById(inst.id));
172
159
  }
173
160
  }
174
- if (ct === 'PARALLEL' || ct?.startsWith('RATIO')) {
161
+ if ((ct === 'PARALLEL' || ct?.startsWith('RATIO')) && !csVeto) {
175
162
  const doing = await this.repo.findDoingTasks(inst.id);
176
163
  if (doing.length > 0)
177
164
  return (await this.repo.findInstanceById(inst.id));
@@ -186,66 +173,176 @@ export class EngineImpl {
186
173
  }
187
174
  // ─── Reject ────────────────────────────────────────────────────────────────
188
175
  async executeAndJumpToEnd(taskId, operator, args = {}) {
189
- const { task, inst } = await this.loadAndCheck(taskId, operator);
190
- const now = new Date();
191
- // 聚合根:废弃所有进行中任务
192
- for (const t of inst.abandonAllDoing(now))
193
- await this.repo.updateTask(t);
194
- // 子实体:完成任务
195
- task.finish(operator, task.variables, now);
196
- await this.repo.updateTask(task);
197
- // v1.0.1:同步回聚合根,避免 updateInstance 级联把任务写回旧状态
198
- syncTaskToAggregate(inst, task);
199
- // 聚合根:驳回
200
- inst.reject(now);
176
+ const { inst } = await this.prepareExecuteTask(taskId, operator, args);
177
+ // 门面 submitType=2 REJECT 唯一入口(对齐 Java executeAndJumpToEnd 语义)
178
+ inst.reject(new Date());
201
179
  await this.repo.updateInstance(inst);
202
180
  await this.fireEvent({ type: EventType.ProcessReject, instanceId: inst.id, taskId, operator });
203
- return inst;
181
+ return (await this.repo.findInstanceById(inst.id));
204
182
  }
205
- // ─── Jump ──────────────────────────────────────────────────────────────────
183
+ // ─── Jump(ROLLBACK 空 target / JUMP 命名 target,boot2 executeAndJumpTask)────
206
184
  async executeAndJumpTask(taskId, operator, args = {}, targetTaskName) {
207
- const { task, inst } = await this.loadAndCheck(taskId, operator);
208
- const now = new Date();
209
- // 聚合根:废弃所有进行中任务
210
- for (const t of inst.abandonAllDoing(now))
211
- await this.repo.updateTask(t);
212
- // 子实体:完成任务
213
- task.finish(operator, task.variables, now);
214
- await this.repo.updateTask(task);
215
- if (targetTaskName) {
216
- const def = await this.repo.findDefineById(inst.defineId);
217
- const flow = JSON.parse(typeof def.content === 'string' ? def.content : new TextDecoder().decode(def.content));
185
+ const { task, inst, flow, vars } = await this.prepareExecuteTask(taskId, operator, args);
186
+ if (!targetTaskName) {
187
+ // issues/79:ROLLBACK 对齐 Java rejectTask——退回上一任务节点(首条输入边 source),
188
+ // 新任务 actor=当前任务完成人(退回操作人);无上一任务节点则不产生新待办
189
+ const prevName = this.previousTaskName(flow, task.taskName);
190
+ if (prevName) {
191
+ const prev = findNode(flow, prevName);
192
+ if (prev) {
193
+ const actors = this.rollbackActors(prev, inst, task);
194
+ await this.createTaskWithActors(prev, inst, operator, vars, actors);
195
+ }
196
+ }
197
+ }
198
+ else {
199
+ // issues/79:对齐 Java——目标节点不存在显式报错(前端 JUMP 无效 taskName 不再静默空操作)
218
200
  const target = findNode(flow, targetTaskName);
219
- if (target)
220
- await this.executeNode(flow, inst, target, operator, inst.variables);
201
+ if (!target)
202
+ throw new Error(`根据节点名称[${targetTaskName}]无法找到节点模型`);
203
+ // 对齐 Java isFirstTaskName:跳首任务节点(start 直接后继)assignee 强制为发起人
204
+ if (target.type === TypeTask && this.isFirstTaskNode(flow, target)) {
205
+ target.properties = target.properties ?? {};
206
+ target.properties.assignee = inst.operator;
207
+ }
208
+ await this.executeNode(flow, inst, target, operator, vars);
221
209
  }
222
- return inst;
210
+ return (await this.repo.findInstanceById(inst.id));
223
211
  }
224
212
  // ─── Jump To First Task(退回发起人,boot2 ROLLBACK_TO_OPERATOR=6)────────────
225
213
  async executeAndJumpToFirstTaskNode(taskId, operator, args = {}) {
226
- const { task, inst } = await this.loadAndCheck(taskId, operator);
227
- const now = new Date();
228
- // 聚合根:废弃所有进行中任务
229
- for (const t of inst.abandonAllDoing(now))
230
- await this.repo.updateTask(t);
231
- // 子实体:完成任务
232
- task.finish(operator, task.variables, now);
233
- await this.repo.updateTask(task);
214
+ const { inst, flow, vars } = await this.prepareExecuteTask(taskId, operator, args);
234
215
  // 找到第一个任务节点,强制参与者为发起人,重新执行
235
- const def = await this.repo.findDefineById(inst.defineId);
236
- const flow = JSON.parse(typeof def.content === 'string' ? def.content : new TextDecoder().decode(def.content));
237
216
  const startNode = findNodeByType(flow, TypeStart);
238
217
  if (startNode) {
239
218
  for (const node of followEdges(flow, startNode.id)) {
240
219
  if (node.type === TypeTask || node.type === TypeCustom) {
241
220
  node.properties = node.properties ?? {};
242
221
  node.properties.assignee = inst.operator;
243
- await this.executeNode(flow, inst, node, operator, inst.variables);
222
+ await this.executeNode(flow, inst, node, operator, vars);
244
223
  break;
245
224
  }
246
225
  }
247
226
  }
248
- return inst;
227
+ return (await this.repo.findInstanceById(inst.id));
228
+ }
229
+ // ─── Execute 公共序言(对齐 Java prepareExecution)──────────────────────────
230
+ // 执行公共序言(对齐 Java prepareExecution):权限校验 → f_ 字段权限过滤 →
231
+ // 完成任务(子实体状态转换 + 实例变量合并,经 updateInstance 级联落库)→
232
+ // 返回流程模型 + 合并后执行变量。Java jump 路径不废弃其余 DOING 任务
233
+ // (会签兄弟任务不受影响),此处保持一致。
234
+ async prepareExecuteTask(taskId, operator, args) {
235
+ const { task, inst } = await this.loadAndCheck(taskId, operator);
236
+ // issues/26:办理提交的 f_ 字段按任务节点字段权限过滤(只读/隐藏不入变量)
237
+ const def = await this.repo.findDefineById(inst.defineId);
238
+ const flow = JSON.parse(typeof def.content === 'string' ? def.content : new TextDecoder().decode(def.content));
239
+ args = filterFieldByPerm(args, findNode(flow, task.taskName));
240
+ const vars = { ...inst.variables, ...task.variables, ...args };
241
+ await this.addUserInfo(operator, vars);
242
+ const now = new Date();
243
+ // 聚合根:完成任务(子实体状态转换 + 实例变量合并)
244
+ inst.completeTask(task, operator, vars, now);
245
+ await this.repo.updateTask(task);
246
+ // v1.0.1:updateInstance 级联持久化依赖聚合内任务副本为最新状态,
247
+ // completeTask 改的是外部任务对象,需同步回聚合根
248
+ syncTaskToAggregate(inst, task);
249
+ await this.fireEvent({ type: EventType.TaskComplete, instanceId: inst.id, taskId: task.id, nodeId: task.taskName, operator });
250
+ inst.variables = vars;
251
+ await this.repo.updateInstance(inst);
252
+ return { task, inst, flow, vars };
253
+ }
254
+ // 当前任务节点的首条输入边 source(issues/79 对齐 Java getPreviousTaskName)
255
+ previousTaskName(flow, taskName) {
256
+ const node = findNode(flow, taskName);
257
+ if (!node)
258
+ return '';
259
+ for (const edge of flow.edges) {
260
+ if (edge.targetNodeId === node.id) {
261
+ const src = findNode(flow, edge.sourceNodeId);
262
+ if (src && (src.type === TypeTask || src.type === TypeCustom))
263
+ return src.id;
264
+ }
265
+ }
266
+ return '';
267
+ }
268
+ // 是否 start 直接后继任务节点(issues/79 对齐 Java FlowUtil.isFirstTaskName)
269
+ isFirstTaskNode(flow, node) {
270
+ const start = findNodeByType(flow, TypeStart);
271
+ if (!start)
272
+ return false;
273
+ return flow.edges.some(e => e.sourceNodeId === start.id && e.targetNodeId === node.id);
274
+ }
275
+ // ROLLBACK 新任务参与者:优先当前任务完成人(退回操作人,对齐 Java rejectTask
276
+ // singletonList(currentTask.getActorId())),其次按目标节点 assignee 解析
277
+ rollbackActors(node, inst, task) {
278
+ if (task.actorId)
279
+ return [task.actorId];
280
+ const nextOp = inst.variables[KeyNextNodeOperator];
281
+ if (nextOp != null) {
282
+ if (typeof nextOp === 'string')
283
+ return nextOp.split(',').map(s => s.trim()).filter(Boolean);
284
+ if (Array.isArray(nextOp))
285
+ return nextOp.map(String);
286
+ return [String(nextOp)];
287
+ }
288
+ const assignee = node.properties?.assignee;
289
+ if (assignee) {
290
+ const actors = [];
291
+ for (const raw of assignee.split(',')) {
292
+ let token = raw.trim();
293
+ if (!token)
294
+ continue;
295
+ if (token.includes('applicant'))
296
+ token = token.replace('applicant', inst.operator);
297
+ if (token in inst.variables) {
298
+ const val = inst.variables[token];
299
+ if (Array.isArray(val))
300
+ actors.push(...val.map(String));
301
+ else
302
+ actors.push(String(val));
303
+ }
304
+ else {
305
+ actors.push(token);
306
+ }
307
+ }
308
+ return actors;
309
+ }
310
+ return [];
311
+ }
312
+ // 以显式参与者建任务(会签节点拆分为逐人任务,对齐 Java 会签创建语义)
313
+ async createTaskWithActors(node, inst, operator, vars, actors) {
314
+ if (!actors.length)
315
+ return;
316
+ const ct = node.properties?.countersignType;
317
+ const now = new Date();
318
+ const form = node.properties?.form ?? '';
319
+ if (isCountersign(node.properties?.performType) && ct) {
320
+ switch (ct) {
321
+ case 'PARALLEL':
322
+ case '':
323
+ for (const actor of actors)
324
+ await this.repo.saveTask(inst.createTask(this.nextId(), node.id, node.text.value, actor, operator, form, now, 1));
325
+ return;
326
+ case 'SEQUENTIAL': {
327
+ const nt = inst.createTask(this.nextId(), node.id, node.text.value, actors[0], operator, form, now, 1);
328
+ nt.variables = {
329
+ [`nrOfInstances_${node.id}`]: actors.length,
330
+ [`loopCounter_${node.id}`]: 0,
331
+ [`operatorList_${node.id}`]: actors,
332
+ };
333
+ await this.repo.saveTask(nt);
334
+ return;
335
+ }
336
+ default:
337
+ for (const actor of actors)
338
+ await this.repo.saveTask(inst.createTask(this.nextId(), node.id, node.text.value, actor, operator, form, now, 1));
339
+ return;
340
+ }
341
+ }
342
+ const nt = inst.createTask(this.nextId(), node.id, node.text.value, actors[0], operator, form, now);
343
+ if (actors.length > 1)
344
+ nt.actorIds = actors;
345
+ await this.repo.saveTask(nt);
249
346
  }
250
347
  // ─── Helpers ───────────────────────────────────────────────────────────────
251
348
  async loadAndCheck(taskId, operator) {
package/dist/facade.d.ts CHANGED
@@ -44,6 +44,13 @@ export declare class JeeflowFacade {
44
44
  private designSave;
45
45
  private surrogatePage;
46
46
  private surrogateSave;
47
+ /** 委托更新(issues/77):按 id 全字段更新,id 缺失/不存在报错 */
48
+ private surrogateUpdate;
49
+ /** 委托详情(issues/77):按 id 查单条,返回行结构(时间格式化) */
50
+ private surrogateDetail;
51
+ /** 委托写入公共字段。授权人(operator)仅在显式传入时覆盖,避免 update
52
+ * 时清空原授权人(前端编辑表单不带 operator;集成层注入时 operator=授权人,覆盖无害) */
53
+ private applySurrogateFields;
47
54
  private getLastByName;
48
55
  private highLight;
49
56
  /** 节点成员进度(issue 41,对齐 boot3 highLight):按任务状态 + 会签变量组装
package/dist/facade.js CHANGED
@@ -101,6 +101,10 @@ export class JeeflowFacade {
101
101
  return this.surrogatePage(args);
102
102
  case 'processSurrogate/save':
103
103
  return this.surrogateSave(args);
104
+ case 'processSurrogate/update': // issues/77
105
+ return this.surrogateUpdate(args);
106
+ case 'processSurrogate/detail': // issues/77
107
+ return this.surrogateDetail(args);
104
108
  case 'processSurrogate/remove':
105
109
  return this.ext().removeSurrogate(toId(args.id));
106
110
  case 'processDefine/getLastByName':
@@ -576,7 +580,8 @@ export class JeeflowFacade {
576
580
  const pageNum = toInt(args.pageNum ?? 1);
577
581
  const pageSize = toInt(args.pageSize ?? 10);
578
582
  const [rows, total] = await this.ext().pageSurrogates(pageNum, pageSize, filters, parseMQuery(args));
579
- return pageData(pageNum, pageSize, total, rows);
583
+ // issues/77:行走 surrogateRowToMap(时间格式化),与 detail 同构
584
+ return pageData(pageNum, pageSize, total, rows.map(r => surrogateRowToMap(r)));
580
585
  }
581
586
  async surrogateSave(args) {
582
587
  const ext = this.ext();
@@ -585,30 +590,56 @@ export class JeeflowFacade {
585
590
  let surrogate;
586
591
  if (!surrogateId) {
587
592
  surrogate = {
588
- id: '', operator, // 授权人 = 操作人
589
- surrogate: String(args.surrogate ?? ''), processName: String(args.processName ?? ''),
590
- enabled: toInt(args.enabled ?? 1),
593
+ id: '', operator, surrogate: '', processName: '', // 授权人 = 操作人(新建必有)
594
+ enabled: 1,
591
595
  createTime: new Date(), createUser: operator,
592
596
  updateTime: new Date(), updateUser: operator,
593
597
  };
598
+ this.applySurrogateFields(surrogate, args, operator);
594
599
  await ext.saveSurrogate(surrogate);
595
600
  }
596
601
  else {
597
602
  const found = await ext.findSurrogateById(surrogateId);
598
603
  if (!found)
599
604
  throw new Error('委托记录不存在');
600
- if (args.surrogate != null)
601
- found.surrogate = String(args.surrogate);
602
- if (args.processName != null)
603
- found.processName = String(args.processName);
604
- if (args.enabled != null)
605
- found.enabled = toInt(args.enabled);
606
- found.updateUser = operator;
605
+ this.applySurrogateFields(found, args, operator);
607
606
  await ext.updateSurrogate(found);
608
607
  surrogate = found;
609
608
  }
610
609
  return { id: surrogate.id };
611
610
  }
611
+ /** 委托更新(issues/77):按 id 全字段更新,id 缺失/不存在报错 */
612
+ async surrogateUpdate(args) {
613
+ const ext = this.ext();
614
+ const surrogateId = toId(args.id);
615
+ const surrogate = await ext.findSurrogateById(surrogateId);
616
+ if (!surrogate)
617
+ throw new Error('委托记录不存在');
618
+ const operator = String(args.operator ?? 'user1');
619
+ this.applySurrogateFields(surrogate, args, operator);
620
+ await ext.updateSurrogate(surrogate);
621
+ return { id: surrogate.id };
622
+ }
623
+ /** 委托详情(issues/77):按 id 查单条,返回行结构(时间格式化) */
624
+ async surrogateDetail(args) {
625
+ const surrogateId = toId(args.id);
626
+ const surrogate = await this.ext().findSurrogateById(surrogateId);
627
+ if (!surrogate)
628
+ throw new Error('委托记录不存在');
629
+ return surrogateRowToMap(surrogate);
630
+ }
631
+ /** 委托写入公共字段。授权人(operator)仅在显式传入时覆盖,避免 update
632
+ * 时清空原授权人(前端编辑表单不带 operator;集成层注入时 operator=授权人,覆盖无害) */
633
+ applySurrogateFields(s, args, operator) {
634
+ s.processName = String(args.processName ?? '');
635
+ if ('operator' in args)
636
+ s.operator = String(args.operator);
637
+ s.surrogate = String(args.surrogate ?? '');
638
+ s.startTime = parseSurrogateTime(args.startTime);
639
+ s.endTime = parseSurrogateTime(args.endTime);
640
+ s.enabled = args.enabled != null ? toInt(args.enabled) : 1;
641
+ s.updateUser = operator;
642
+ }
612
643
  // ── 视图端点(v1.2.0) ──────────────────────────────────────────────────
613
644
  async getLastByName(args) {
614
645
  const def = await this.repo.findDefineByName(String(args.processDefineName ?? ''));
@@ -805,12 +836,18 @@ export class JeeflowFacade {
805
836
  if (!task)
806
837
  throw new Error('任务不存在');
807
838
  const actors = await this.repo.findTaskActors(taskId);
839
+ // issues/82-5:任务级 ext.isFirstTaskNode(前端 detail.vue 双兜底 record.ext?.isFirstTaskNode)
840
+ // 首个任务节点且 DOING → true,与 instance detail 的 activeTaskList 行语义一致
841
+ const tExt = { ...(task.variables ?? {}) };
842
+ const doing = task.taskState === TaskState.Doing;
843
+ tExt.isFirstTaskNode = false;
808
844
  const vo = {
809
845
  id: task.id, processInstanceId: task.processInstanceId, taskName: task.taskName,
810
846
  displayName: task.displayName, taskType: task.taskType ?? null,
811
847
  performType: task.performType ?? null, taskState: task.taskState,
812
848
  operator: task.actorId ?? '', formKey: task.formKey ?? '',
813
849
  taskActorIdList: actors, executable: task.isAllowed(operator),
850
+ ext: tExt,
814
851
  taskFormData: formDataOf(task.variables, 'tf_'), // issues/15
815
852
  };
816
853
  // taskModel:流程定义中对应节点
@@ -819,6 +856,7 @@ export class JeeflowFacade {
819
856
  const def = await this.repo.findDefineById(inst.defineId);
820
857
  if (def) {
821
858
  vo.jsonObject = this.parseGraph(def.content); // issues/05
859
+ tExt.isFirstTaskNode = doing && task.taskName === this.firstTaskNodeId(vo.jsonObject);
822
860
  try {
823
861
  const flow = JSON.parse(toStr(def.content));
824
862
  for (const n of flow.nodes ?? []) {
@@ -874,7 +912,8 @@ export class JeeflowFacade {
874
912
  catch { /* ignore */ }
875
913
  }
876
914
  if (candidates.length > 0) {
877
- const rows = candidates.map(c => ({ userId: c, realName: c }));
915
+ // issues/80:行键对齐前端 UserSelect(valueField='id')——补 id 键,保留 userId 兼容旧消费方
916
+ const rows = candidates.map(c => ({ id: c, userId: c, realName: c }));
878
917
  return pageData(pageNum, pageSize, rows.length, rows);
879
918
  }
880
919
  // 无模型候选 → 用户分页搜索(依赖 userSearch 钩子)
@@ -1102,6 +1141,37 @@ function defineRowToMap(r) {
1102
1141
  };
1103
1142
  }
1104
1143
  /** 设计行:时间格式化(issues/63) */
1144
+ /** 委托行:时间格式化(issues/77,对齐 Java surrogateRowToMap / Go surrogateRowToMap / SPEC) */
1145
+ function surrogateRowToMap(s) {
1146
+ return {
1147
+ id: s.id, processName: s.processName, operator: s.operator, surrogate: s.surrogate,
1148
+ startTime: fmtTime(s.startTime ?? null), endTime: fmtTime(s.endTime ?? null),
1149
+ enabled: s.enabled,
1150
+ createTime: fmtTime(s.createTime ?? null), createUser: s.createUser,
1151
+ updateTime: fmtTime(s.updateTime ?? null), updateUser: s.updateUser,
1152
+ };
1153
+ }
1154
+ /** 解析委托时间入参:兼容 yyyy-MM-dd HH:mm:ss(前端 RangePicker/SPEC 契约)与 ISO T(issues/77) */
1155
+ function parseSurrogateTime(v) {
1156
+ if (v == null)
1157
+ return undefined;
1158
+ const s = String(v).trim();
1159
+ if (!s)
1160
+ return undefined;
1161
+ for (const fmt of ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DDTHH:mm:ss', 'YYYY-MM-DD']) {
1162
+ const m = s.match(fmt === 'YYYY-MM-DD'
1163
+ ? /^(\d{4})-(\d{2})-(\d{2})$/
1164
+ : /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})$/);
1165
+ if (!m)
1166
+ continue;
1167
+ if (fmt === 'YYYY-MM-DD')
1168
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
1169
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]));
1170
+ }
1171
+ // 兜底:交给 Date 解析(覆盖其它可解析形态),失败返回 undefined
1172
+ const d = new Date(s);
1173
+ return isNaN(d.getTime()) ? undefined : d;
1174
+ }
1105
1175
  function designRowToMap(r) {
1106
1176
  return {
1107
1177
  id: r.id, name: r.name, displayName: r.displayName,
@@ -1199,6 +1269,12 @@ function toId(v) {
1199
1269
  // 数字输入(JS 安全整数内)转字符串;字符串输入原样保留(含超长雪花 id)。
1200
1270
  if (v == null)
1201
1271
  throw new Error('id 缺失或非法');
1272
+ // issues/82 负向(对齐 Go TestSnowflakeIDPrecision / issues/38 E9):数字 id 超 2^53
1273
+ // 说明它已被 JSON 解析器(JSON.parse / encoding/json)降级为 float64 且精度已丢,
1274
+ // 必须显性报错而非 String() 静默截断成错误 id。
1275
+ if (typeof v === 'number' && Math.abs(v) > 2 ** 53) {
1276
+ throw new Error(`id ${v} 超出 float64 精确范围(2^53),请以字符串传递`);
1277
+ }
1202
1278
  const s = String(v).trim();
1203
1279
  if (!/^\d+$/.test(s) || s === '0')
1204
1280
  throw new Error('id 缺失或非法');
package/dist/jdbc/ext.js CHANGED
@@ -203,8 +203,7 @@ export class JdbcProcessExtRepository {
203
203
  s.createTime = now;
204
204
  if (!s.updateTime)
205
205
  s.updateTime = now;
206
- if (!s.enabled)
207
- s.enabled = 1;
206
+ // 显式 enabled=0 是合法值(停用委托);缺省由门面处理(对齐 Java/Go/Python,issues/82-7)
208
207
  const conn = await this.c();
209
208
  try {
210
209
  await conn.execute(this.sql('INSERT INTO wf_process_surrogate (id, process_name, operator, surrogate, start_time, ' +
@@ -71,8 +71,7 @@ export class MemoryExtRepository {
71
71
  s.createTime = now;
72
72
  if (!s.updateTime)
73
73
  s.updateTime = now;
74
- if (!s.enabled)
75
- s.enabled = 1;
74
+ // 显式 enabled=0 是合法值(停用委托);缺省由门面处理(对齐 Java/Go/Python,issues/82-7)
76
75
  this.surrogates.set(s.id, { ...s });
77
76
  }
78
77
  async updateSurrogate(s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.8.17",
3
+ "version": "1.8.19",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",