@fieldwangai/agentflow 0.1.136 → 0.1.137

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.
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import { getAgentflowDataRoot } from "./paths.mjs";
5
5
  import { getTeamForUser } from "./teams.mjs";
6
6
 
7
- const REGISTRY_VERSION = 1;
7
+ const REGISTRY_VERSION = 2;
8
8
 
9
9
  function registryPath() {
10
10
  return path.join(getAgentflowDataRoot(), "collaboration", "prd-workflows.json");
@@ -49,10 +49,45 @@ function normalizeShareToken(value) {
49
49
  return String(value || "").trim();
50
50
  }
51
51
 
52
+ function normalizeMemberRole(value) {
53
+ const role = String(value || "").trim().toLowerCase();
54
+ if (role === "reporter" || role === "editor") return "reporter";
55
+ if (role === "viewer") return "viewer";
56
+ return "";
57
+ }
58
+
59
+ function normalizedMemberMap(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
61
+ return Object.fromEntries(Object.entries(value)
62
+ .map(([userId, role]) => [normalizeUserId(userId), normalizeMemberRole(role)])
63
+ .filter(([userId, role]) => userId && role));
64
+ }
65
+
66
+ function collaborationMembers(record) {
67
+ const ownerId = normalizeUserId(record?.ownerId);
68
+ const explicit = normalizedMemberMap(record?.members);
69
+ const derived = normalizedMemberMap(record?.derivedMembers);
70
+ const rows = new Map();
71
+ if (ownerId) rows.set(ownerId, {
72
+ userId: ownerId,
73
+ role: "owner",
74
+ source: String(record?.ownerSource || "legacy"),
75
+ });
76
+ for (const [userId, role] of Object.entries(derived)) {
77
+ if (userId === ownerId) continue;
78
+ rows.set(userId, { userId, role: role === "reporter" ? "reporter" : "viewer", source: "tapd" });
79
+ }
80
+ for (const [userId, role] of Object.entries(explicit)) {
81
+ if (userId === ownerId) continue;
82
+ rows.set(userId, { userId, role, source: "explicit" });
83
+ }
84
+ return [...rows.values()];
85
+ }
86
+
52
87
  function publicWorkflow(record, userId = "") {
53
88
  if (!record) return null;
54
89
  const actorId = normalizeUserId(userId);
55
- const members = record.members && typeof record.members === "object" ? record.members : {};
90
+ const members = collaborationMembers(record);
56
91
  const access = prdWorkflowCollaborationAccess(record, actorId);
57
92
  return {
58
93
  id: record.id,
@@ -61,8 +96,17 @@ function publicWorkflow(record, userId = "") {
61
96
  role: access.role,
62
97
  accessSource: access.source,
63
98
  teamId: String(record.teamId || getTeamForUser(record.ownerId)?.id || ""),
64
- memberCount: Object.keys(members).length,
65
- members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
99
+ ownerSource: String(record.ownerSource || "legacy"),
100
+ memberCount: members.length,
101
+ members,
102
+ authority: record.authority && typeof record.authority === "object" ? {
103
+ type: String(record.authority.type || ""),
104
+ observedAt: String(record.authority.observedAt || ""),
105
+ revision: String(record.authority.revision || ""),
106
+ unresolvedParticipants: Array.isArray(record.authority.unresolvedParticipants)
107
+ ? record.authority.unresolvedParticipants.map((value) => String(value || "")).filter(Boolean)
108
+ : [],
109
+ } : null,
66
110
  shareActive: Boolean(record.shareToken),
67
111
  shareCreatedAt: record.shareCreatedAt || "",
68
112
  createdAt: record.createdAt || "",
@@ -73,16 +117,26 @@ function publicWorkflow(record, userId = "") {
73
117
  export function prdWorkflowCollaborationAccess(record, userId) {
74
118
  if (!record) return { allowed: true, role: "" };
75
119
  const actorId = normalizeUserId(userId);
76
- const directRole = actorId === record.ownerId ? "owner" : String(record.members?.[actorId] || "");
120
+ const explicitRole = normalizeMemberRole(record.members?.[actorId]);
121
+ const derivedRole = normalizeMemberRole(record.derivedMembers?.[actorId]);
122
+ const directRole = actorId === record.ownerId ? "owner" : (explicitRole || derivedRole);
77
123
  const actorTeam = getTeamForUser(actorId);
78
124
  const recordTeamId = String(record.teamId || getTeamForUser(record.ownerId)?.id || "");
79
125
  const teamRole = actorTeam?.status === "active" && actorTeam.id === recordTeamId ? "viewer" : "";
80
126
  const role = directRole || teamRole;
81
127
  return {
82
- allowed: role === "owner" || role === "editor" || role === "viewer",
83
- writable: role === "owner" || role === "editor",
128
+ allowed: role === "owner" || role === "reporter" || role === "viewer",
129
+ writable: role === "owner" || role === "reporter",
84
130
  role,
85
- source: directRole ? "member" : teamRole ? "team" : "",
131
+ source: actorId === record.ownerId
132
+ ? String(record.ownerSource || "legacy")
133
+ : explicitRole
134
+ ? "explicit"
135
+ : derivedRole
136
+ ? "tapd"
137
+ : teamRole
138
+ ? "team"
139
+ : "",
86
140
  teamId: teamRole ? recordTeamId : "",
87
141
  };
88
142
  }
@@ -91,6 +145,14 @@ export function getPrdWorkflowCollaborationById(workflowId) {
91
145
  return readRegistry().workflows[String(workflowId || "").trim()] || null;
92
146
  }
93
147
 
148
+ export function getPrdWorkflowCollaborationByTapdId(tapdId) {
149
+ const normalizedTapdId = normalizeTapdId(tapdId);
150
+ if (!normalizedTapdId) return null;
151
+ return Object.values(readRegistry().workflows)
152
+ .filter((record) => record?.tapdId === normalizedTapdId)
153
+ .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")))[0] || null;
154
+ }
155
+
94
156
  export function getPrdWorkflowCollaborationByShareToken(shareToken) {
95
157
  const token = normalizeShareToken(shareToken);
96
158
  if (!token) return null;
@@ -120,7 +182,9 @@ export function listPrdWorkflowCollaborationsForUser(userId) {
120
182
  if (!actorId) return [];
121
183
  return Object.values(readRegistry().workflows)
122
184
  .filter((record) => (
123
- record?.ownerId === actorId || Boolean(record?.members?.[actorId])
185
+ record?.ownerId === actorId
186
+ || Boolean(normalizeMemberRole(record?.members?.[actorId]))
187
+ || Boolean(normalizeMemberRole(record?.derivedMembers?.[actorId]))
124
188
  ))
125
189
  .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
126
190
  }
@@ -139,18 +203,23 @@ export function ensurePrdWorkflowCollaboration({ tapdId, userId }) {
139
203
  if (!ownerId) return { error: "Authentication required", status: 401 };
140
204
  if (!normalizedTapdId) return { error: "Missing tapdId", status: 400 };
141
205
  const registry = readRegistry();
142
- let record = Object.values(registry.workflows).find((item) => (
143
- item?.tapdId === normalizedTapdId && item?.ownerId === ownerId
144
- )) || null;
145
- if (record) return { record, workflow: publicWorkflow(record, ownerId), created: false };
206
+ let record = Object.values(registry.workflows).find((item) => item?.tapdId === normalizedTapdId) || null;
207
+ if (record) {
208
+ const access = prdWorkflowCollaborationAccess(record, ownerId);
209
+ if (!access.allowed) return { error: "Workflow already belongs to another owner", status: 403 };
210
+ return { record, workflow: publicWorkflow(record, ownerId), created: false };
211
+ }
146
212
  const now = new Date().toISOString();
147
213
  const workflowId = `prd_${crypto.randomBytes(12).toString("hex")}`;
148
214
  record = {
149
215
  id: workflowId,
150
216
  tapdId: normalizedTapdId,
151
217
  ownerId,
218
+ ownerSource: "legacy",
219
+ stateOwnerId: ownerId,
152
220
  teamId: String(getTeamForUser(ownerId)?.id || ""),
153
- members: { [ownerId]: "owner" },
221
+ members: {},
222
+ derivedMembers: {},
154
223
  createdAt: now,
155
224
  updatedAt: now,
156
225
  };
@@ -219,7 +288,7 @@ export function addPrdWorkflowCollaborationMember({
219
288
  workflowId,
220
289
  userId,
221
290
  memberUserId,
222
- role = "editor",
291
+ role = "reporter",
223
292
  }) {
224
293
  const registry = readRegistry();
225
294
  const record = registry.workflows[String(workflowId || "").trim()];
@@ -233,6 +302,10 @@ export function addPrdWorkflowCollaborationMember({
233
302
  if (targetId === record.ownerId) {
234
303
  return { workflow: publicWorkflow(record, actorId), unchanged: true };
235
304
  }
305
+ const normalizedRole = normalizeMemberRole(role);
306
+ if (!normalizedRole) {
307
+ return { error: "Workflow member role must be reporter or viewer", status: 400 };
308
+ }
236
309
  const conflicting = Object.values(registry.workflows).find((item) => (
237
310
  item?.id !== record.id
238
311
  && item?.tapdId === record.tapdId
@@ -242,12 +315,92 @@ export function addPrdWorkflowCollaborationMember({
242
315
  return { error: "该用户已经加入同一 TAPD ID 的另一个 Workflow 分享", status: 409 };
243
316
  }
244
317
  record.members = record.members && typeof record.members === "object" ? record.members : {};
245
- record.members[targetId] = role === "viewer" ? "viewer" : "editor";
318
+ record.members[targetId] = normalizedRole;
246
319
  record.updatedAt = new Date().toISOString();
247
320
  writeRegistry(registry);
248
321
  return { workflow: publicWorkflow(record, actorId), memberUserId: targetId };
249
322
  }
250
323
 
324
+ export function syncPrdWorkflowAuthority({
325
+ tapdId,
326
+ userId,
327
+ isAdmin = false,
328
+ authority = "tapd",
329
+ ownerUserId,
330
+ ownerIdentity = "",
331
+ participantUserIds = [],
332
+ participantIdentities = [],
333
+ unresolvedParticipants = [],
334
+ observedAt = "",
335
+ revision = "",
336
+ }) {
337
+ const actorId = normalizeUserId(userId);
338
+ const normalizedTapdId = normalizeTapdId(tapdId);
339
+ const normalizedOwnerId = normalizeUserId(ownerUserId);
340
+ const authorityType = String(authority || "tapd").trim().toLowerCase();
341
+ if (!actorId) return { error: "Authentication required", status: 401 };
342
+ if (!normalizedTapdId) return { error: "Missing tapdId", status: 400 };
343
+ if (authorityType !== "tapd") return { error: `Unsupported Workflow authority: ${authorityType}`, status: 400 };
344
+ if (!normalizedOwnerId) return { error: "TAPD owner must be a registered AgentFlow user", status: 422 };
345
+ const normalizedObservedAt = String(observedAt || "").trim();
346
+ if (normalizedObservedAt && !Number.isFinite(Date.parse(normalizedObservedAt))) {
347
+ return { error: "observedAt must be an ISO-compatible date", status: 400 };
348
+ }
349
+
350
+ const registry = readRegistry();
351
+ let record = Object.values(registry.workflows).find((item) => item?.tapdId === normalizedTapdId) || null;
352
+ const previousOwnerId = normalizeUserId(record?.ownerId);
353
+ if (!record && actorId !== normalizedOwnerId && isAdmin !== true) {
354
+ return { error: "Only the TAPD owner can initialize Workflow permissions", status: 403 };
355
+ }
356
+ if (record && actorId !== previousOwnerId && isAdmin !== true) {
357
+ return { error: "Only the current Workflow owner or an administrator can synchronize TAPD permissions", status: 403 };
358
+ }
359
+ const storedObservedAt = String(record?.authority?.observedAt || "").trim();
360
+ if (storedObservedAt && normalizedObservedAt && Date.parse(normalizedObservedAt) < Date.parse(storedObservedAt)) {
361
+ return { error: "TAPD permission snapshot is older than the stored snapshot", status: 409 };
362
+ }
363
+
364
+ const now = new Date().toISOString();
365
+ if (!record) {
366
+ const workflowId = `prd_${crypto.randomBytes(12).toString("hex")}`;
367
+ record = {
368
+ id: workflowId,
369
+ tapdId: normalizedTapdId,
370
+ stateOwnerId: normalizedOwnerId,
371
+ members: {},
372
+ createdAt: now,
373
+ };
374
+ registry.workflows[workflowId] = record;
375
+ }
376
+ const participants = [...new Set(participantUserIds.map(normalizeUserId).filter(Boolean))]
377
+ .filter((id) => id !== normalizedOwnerId);
378
+ record.ownerId = normalizedOwnerId;
379
+ record.stateOwnerId = normalizeUserId(record.stateOwnerId || previousOwnerId || normalizedOwnerId);
380
+ record.ownerSource = "tapd";
381
+ record.teamId = String(getTeamForUser(normalizedOwnerId)?.id || "");
382
+ record.members = normalizedMemberMap(record.members);
383
+ delete record.members[normalizedOwnerId];
384
+ record.derivedMembers = Object.fromEntries(participants.map((id) => [id, "viewer"]));
385
+ record.authority = {
386
+ type: "tapd",
387
+ ownerIdentity: String(ownerIdentity || "").trim(),
388
+ participantIdentities: [...new Set(participantIdentities.map((value) => String(value || "").trim()).filter(Boolean))],
389
+ unresolvedParticipants: [...new Set(unresolvedParticipants.map((value) => String(value || "").trim()).filter(Boolean))],
390
+ observedAt: normalizedObservedAt || now,
391
+ revision: String(revision || "").trim(),
392
+ };
393
+ record.updatedAt = now;
394
+ writeRegistry(registry);
395
+ return {
396
+ record,
397
+ workflow: publicWorkflow(record, actorId),
398
+ created: !previousOwnerId,
399
+ ownerChanged: Boolean(previousOwnerId && previousOwnerId !== normalizedOwnerId),
400
+ previousOwnerId,
401
+ };
402
+ }
403
+
251
404
  export function removePrdWorkflowCollaborationMember({
252
405
  workflowId,
253
406
  userId,
@@ -270,9 +423,11 @@ export function removePrdWorkflowCollaborationMember({
270
423
  delete record.members[targetId];
271
424
  record.updatedAt = new Date().toISOString();
272
425
  writeRegistry(registry);
426
+ const selfRemoved = targetId === actorId;
427
+ const stillAllowed = prdWorkflowCollaborationAccess(record, actorId).allowed;
273
428
  return {
274
429
  workflow: publicWorkflow(record, actorId),
275
430
  removedUserId: targetId,
276
- left: targetId === actorId,
431
+ left: selfRemoved && !stillAllowed,
277
432
  };
278
433
  }