@fieldwangai/agentflow 0.1.136 → 0.1.138

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 = 3;
8
8
 
9
9
  function registryPath() {
10
10
  return path.join(getAgentflowDataRoot(), "collaboration", "prd-workflows.json");
@@ -49,10 +49,65 @@ 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 normalizeKnowledgeBindings(value) {
67
+ if (!Array.isArray(value)) return [];
68
+ const seen = new Set();
69
+ return value.flatMap((entry) => {
70
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
71
+ const workspaceId = String(entry.workspaceId || entry.id || "").trim();
72
+ if (!workspaceId || seen.has(workspaceId)) return [];
73
+ seen.add(workspaceId);
74
+ return [{
75
+ workspaceId,
76
+ label: String(entry.label || workspaceId).trim() || workspaceId,
77
+ kind: String(entry.kind || "git").trim().toLowerCase(),
78
+ type: String(entry.type || "knowledge").trim().toLowerCase(),
79
+ branch: String(entry.branch || "").trim(),
80
+ boundBy: normalizeUserId(entry.boundBy),
81
+ boundAt: String(entry.boundAt || "").trim(),
82
+ }];
83
+ });
84
+ }
85
+
86
+ function collaborationMembers(record) {
87
+ const ownerId = normalizeUserId(record?.ownerId);
88
+ const explicit = normalizedMemberMap(record?.members);
89
+ const derived = normalizedMemberMap(record?.derivedMembers);
90
+ const rows = new Map();
91
+ if (ownerId) rows.set(ownerId, {
92
+ userId: ownerId,
93
+ role: "owner",
94
+ source: String(record?.ownerSource || "legacy"),
95
+ });
96
+ for (const [userId, role] of Object.entries(derived)) {
97
+ if (userId === ownerId) continue;
98
+ rows.set(userId, { userId, role: role === "reporter" ? "reporter" : "viewer", source: "tapd" });
99
+ }
100
+ for (const [userId, role] of Object.entries(explicit)) {
101
+ if (userId === ownerId) continue;
102
+ rows.set(userId, { userId, role, source: "explicit" });
103
+ }
104
+ return [...rows.values()];
105
+ }
106
+
52
107
  function publicWorkflow(record, userId = "") {
53
108
  if (!record) return null;
54
109
  const actorId = normalizeUserId(userId);
55
- const members = record.members && typeof record.members === "object" ? record.members : {};
110
+ const members = collaborationMembers(record);
56
111
  const access = prdWorkflowCollaborationAccess(record, actorId);
57
112
  return {
58
113
  id: record.id,
@@ -61,8 +116,18 @@ function publicWorkflow(record, userId = "") {
61
116
  role: access.role,
62
117
  accessSource: access.source,
63
118
  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 })),
119
+ ownerSource: String(record.ownerSource || "legacy"),
120
+ memberCount: members.length,
121
+ members,
122
+ authority: record.authority && typeof record.authority === "object" ? {
123
+ type: String(record.authority.type || ""),
124
+ observedAt: String(record.authority.observedAt || ""),
125
+ revision: String(record.authority.revision || ""),
126
+ unresolvedParticipants: Array.isArray(record.authority.unresolvedParticipants)
127
+ ? record.authority.unresolvedParticipants.map((value) => String(value || "")).filter(Boolean)
128
+ : [],
129
+ } : null,
130
+ knowledgeBindings: normalizeKnowledgeBindings(record.knowledgeBindings),
66
131
  shareActive: Boolean(record.shareToken),
67
132
  shareCreatedAt: record.shareCreatedAt || "",
68
133
  createdAt: record.createdAt || "",
@@ -73,16 +138,26 @@ function publicWorkflow(record, userId = "") {
73
138
  export function prdWorkflowCollaborationAccess(record, userId) {
74
139
  if (!record) return { allowed: true, role: "" };
75
140
  const actorId = normalizeUserId(userId);
76
- const directRole = actorId === record.ownerId ? "owner" : String(record.members?.[actorId] || "");
141
+ const explicitRole = normalizeMemberRole(record.members?.[actorId]);
142
+ const derivedRole = normalizeMemberRole(record.derivedMembers?.[actorId]);
143
+ const directRole = actorId === record.ownerId ? "owner" : (explicitRole || derivedRole);
77
144
  const actorTeam = getTeamForUser(actorId);
78
145
  const recordTeamId = String(record.teamId || getTeamForUser(record.ownerId)?.id || "");
79
146
  const teamRole = actorTeam?.status === "active" && actorTeam.id === recordTeamId ? "viewer" : "";
80
147
  const role = directRole || teamRole;
81
148
  return {
82
- allowed: role === "owner" || role === "editor" || role === "viewer",
83
- writable: role === "owner" || role === "editor",
149
+ allowed: role === "owner" || role === "reporter" || role === "viewer",
150
+ writable: role === "owner" || role === "reporter",
84
151
  role,
85
- source: directRole ? "member" : teamRole ? "team" : "",
152
+ source: actorId === record.ownerId
153
+ ? String(record.ownerSource || "legacy")
154
+ : explicitRole
155
+ ? "explicit"
156
+ : derivedRole
157
+ ? "tapd"
158
+ : teamRole
159
+ ? "team"
160
+ : "",
86
161
  teamId: teamRole ? recordTeamId : "",
87
162
  };
88
163
  }
@@ -91,6 +166,14 @@ export function getPrdWorkflowCollaborationById(workflowId) {
91
166
  return readRegistry().workflows[String(workflowId || "").trim()] || null;
92
167
  }
93
168
 
169
+ export function getPrdWorkflowCollaborationByTapdId(tapdId) {
170
+ const normalizedTapdId = normalizeTapdId(tapdId);
171
+ if (!normalizedTapdId) return null;
172
+ return Object.values(readRegistry().workflows)
173
+ .filter((record) => record?.tapdId === normalizedTapdId)
174
+ .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")))[0] || null;
175
+ }
176
+
94
177
  export function getPrdWorkflowCollaborationByShareToken(shareToken) {
95
178
  const token = normalizeShareToken(shareToken);
96
179
  if (!token) return null;
@@ -120,7 +203,9 @@ export function listPrdWorkflowCollaborationsForUser(userId) {
120
203
  if (!actorId) return [];
121
204
  return Object.values(readRegistry().workflows)
122
205
  .filter((record) => (
123
- record?.ownerId === actorId || Boolean(record?.members?.[actorId])
206
+ record?.ownerId === actorId
207
+ || Boolean(normalizeMemberRole(record?.members?.[actorId]))
208
+ || Boolean(normalizeMemberRole(record?.derivedMembers?.[actorId]))
124
209
  ))
125
210
  .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
126
211
  }
@@ -139,18 +224,23 @@ export function ensurePrdWorkflowCollaboration({ tapdId, userId }) {
139
224
  if (!ownerId) return { error: "Authentication required", status: 401 };
140
225
  if (!normalizedTapdId) return { error: "Missing tapdId", status: 400 };
141
226
  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 };
227
+ let record = Object.values(registry.workflows).find((item) => item?.tapdId === normalizedTapdId) || null;
228
+ if (record) {
229
+ const access = prdWorkflowCollaborationAccess(record, ownerId);
230
+ if (!access.allowed) return { error: "Workflow already belongs to another owner", status: 403 };
231
+ return { record, workflow: publicWorkflow(record, ownerId), created: false };
232
+ }
146
233
  const now = new Date().toISOString();
147
234
  const workflowId = `prd_${crypto.randomBytes(12).toString("hex")}`;
148
235
  record = {
149
236
  id: workflowId,
150
237
  tapdId: normalizedTapdId,
151
238
  ownerId,
239
+ ownerSource: "legacy",
240
+ stateOwnerId: ownerId,
152
241
  teamId: String(getTeamForUser(ownerId)?.id || ""),
153
- members: { [ownerId]: "owner" },
242
+ members: {},
243
+ derivedMembers: {},
154
244
  createdAt: now,
155
245
  updatedAt: now,
156
246
  };
@@ -163,6 +253,31 @@ export function prdWorkflowCollaborationSummary(record, userId) {
163
253
  return publicWorkflow(record, userId);
164
254
  }
165
255
 
256
+ export function setPrdWorkflowKnowledgeBindings({ tapdId, userId, bindings = [] }) {
257
+ const record = getPrdWorkflowCollaborationForUser(tapdId, userId);
258
+ if (!record) return { error: "PRD Workflow collaboration not found", status: 404 };
259
+ const actorId = normalizeUserId(userId);
260
+ if (prdWorkflowCollaborationAccess(record, actorId).role !== "owner") {
261
+ return { error: "Only the Workflow owner can manage knowledge bindings", status: 403 };
262
+ }
263
+ const registry = readRegistry();
264
+ const stored = registry.workflows[record.id];
265
+ if (!stored) return { error: "PRD Workflow collaboration not found", status: 404 };
266
+ const now = new Date().toISOString();
267
+ stored.knowledgeBindings = normalizeKnowledgeBindings(bindings).map((binding) => ({
268
+ ...binding,
269
+ boundBy: actorId,
270
+ boundAt: binding.boundAt || now,
271
+ }));
272
+ stored.updatedAt = now;
273
+ writeRegistry(registry);
274
+ return {
275
+ record: stored,
276
+ workflow: publicWorkflow(stored, actorId),
277
+ knowledgeBindings: normalizeKnowledgeBindings(stored.knowledgeBindings),
278
+ };
279
+ }
280
+
166
281
  export function ensurePrdWorkflowShareLink({ tapdId, userId }) {
167
282
  const ensured = ensurePrdWorkflowCollaboration({ tapdId, userId });
168
283
  if (ensured.error) return ensured;
@@ -219,7 +334,7 @@ export function addPrdWorkflowCollaborationMember({
219
334
  workflowId,
220
335
  userId,
221
336
  memberUserId,
222
- role = "editor",
337
+ role = "reporter",
223
338
  }) {
224
339
  const registry = readRegistry();
225
340
  const record = registry.workflows[String(workflowId || "").trim()];
@@ -233,6 +348,10 @@ export function addPrdWorkflowCollaborationMember({
233
348
  if (targetId === record.ownerId) {
234
349
  return { workflow: publicWorkflow(record, actorId), unchanged: true };
235
350
  }
351
+ const normalizedRole = normalizeMemberRole(role);
352
+ if (!normalizedRole) {
353
+ return { error: "Workflow member role must be reporter or viewer", status: 400 };
354
+ }
236
355
  const conflicting = Object.values(registry.workflows).find((item) => (
237
356
  item?.id !== record.id
238
357
  && item?.tapdId === record.tapdId
@@ -242,12 +361,92 @@ export function addPrdWorkflowCollaborationMember({
242
361
  return { error: "该用户已经加入同一 TAPD ID 的另一个 Workflow 分享", status: 409 };
243
362
  }
244
363
  record.members = record.members && typeof record.members === "object" ? record.members : {};
245
- record.members[targetId] = role === "viewer" ? "viewer" : "editor";
364
+ record.members[targetId] = normalizedRole;
246
365
  record.updatedAt = new Date().toISOString();
247
366
  writeRegistry(registry);
248
367
  return { workflow: publicWorkflow(record, actorId), memberUserId: targetId };
249
368
  }
250
369
 
370
+ export function syncPrdWorkflowAuthority({
371
+ tapdId,
372
+ userId,
373
+ isAdmin = false,
374
+ authority = "tapd",
375
+ ownerUserId,
376
+ ownerIdentity = "",
377
+ participantUserIds = [],
378
+ participantIdentities = [],
379
+ unresolvedParticipants = [],
380
+ observedAt = "",
381
+ revision = "",
382
+ }) {
383
+ const actorId = normalizeUserId(userId);
384
+ const normalizedTapdId = normalizeTapdId(tapdId);
385
+ const normalizedOwnerId = normalizeUserId(ownerUserId);
386
+ const authorityType = String(authority || "tapd").trim().toLowerCase();
387
+ if (!actorId) return { error: "Authentication required", status: 401 };
388
+ if (!normalizedTapdId) return { error: "Missing tapdId", status: 400 };
389
+ if (authorityType !== "tapd") return { error: `Unsupported Workflow authority: ${authorityType}`, status: 400 };
390
+ if (!normalizedOwnerId) return { error: "TAPD owner must be a registered AgentFlow user", status: 422 };
391
+ const normalizedObservedAt = String(observedAt || "").trim();
392
+ if (normalizedObservedAt && !Number.isFinite(Date.parse(normalizedObservedAt))) {
393
+ return { error: "observedAt must be an ISO-compatible date", status: 400 };
394
+ }
395
+
396
+ const registry = readRegistry();
397
+ let record = Object.values(registry.workflows).find((item) => item?.tapdId === normalizedTapdId) || null;
398
+ const previousOwnerId = normalizeUserId(record?.ownerId);
399
+ if (!record && actorId !== normalizedOwnerId && isAdmin !== true) {
400
+ return { error: "Only the TAPD owner can initialize Workflow permissions", status: 403 };
401
+ }
402
+ if (record && actorId !== previousOwnerId && isAdmin !== true) {
403
+ return { error: "Only the current Workflow owner or an administrator can synchronize TAPD permissions", status: 403 };
404
+ }
405
+ const storedObservedAt = String(record?.authority?.observedAt || "").trim();
406
+ if (storedObservedAt && normalizedObservedAt && Date.parse(normalizedObservedAt) < Date.parse(storedObservedAt)) {
407
+ return { error: "TAPD permission snapshot is older than the stored snapshot", status: 409 };
408
+ }
409
+
410
+ const now = new Date().toISOString();
411
+ if (!record) {
412
+ const workflowId = `prd_${crypto.randomBytes(12).toString("hex")}`;
413
+ record = {
414
+ id: workflowId,
415
+ tapdId: normalizedTapdId,
416
+ stateOwnerId: normalizedOwnerId,
417
+ members: {},
418
+ createdAt: now,
419
+ };
420
+ registry.workflows[workflowId] = record;
421
+ }
422
+ const participants = [...new Set(participantUserIds.map(normalizeUserId).filter(Boolean))]
423
+ .filter((id) => id !== normalizedOwnerId);
424
+ record.ownerId = normalizedOwnerId;
425
+ record.stateOwnerId = normalizeUserId(record.stateOwnerId || previousOwnerId || normalizedOwnerId);
426
+ record.ownerSource = "tapd";
427
+ record.teamId = String(getTeamForUser(normalizedOwnerId)?.id || "");
428
+ record.members = normalizedMemberMap(record.members);
429
+ delete record.members[normalizedOwnerId];
430
+ record.derivedMembers = Object.fromEntries(participants.map((id) => [id, "viewer"]));
431
+ record.authority = {
432
+ type: "tapd",
433
+ ownerIdentity: String(ownerIdentity || "").trim(),
434
+ participantIdentities: [...new Set(participantIdentities.map((value) => String(value || "").trim()).filter(Boolean))],
435
+ unresolvedParticipants: [...new Set(unresolvedParticipants.map((value) => String(value || "").trim()).filter(Boolean))],
436
+ observedAt: normalizedObservedAt || now,
437
+ revision: String(revision || "").trim(),
438
+ };
439
+ record.updatedAt = now;
440
+ writeRegistry(registry);
441
+ return {
442
+ record,
443
+ workflow: publicWorkflow(record, actorId),
444
+ created: !previousOwnerId,
445
+ ownerChanged: Boolean(previousOwnerId && previousOwnerId !== normalizedOwnerId),
446
+ previousOwnerId,
447
+ };
448
+ }
449
+
251
450
  export function removePrdWorkflowCollaborationMember({
252
451
  workflowId,
253
452
  userId,
@@ -270,9 +469,11 @@ export function removePrdWorkflowCollaborationMember({
270
469
  delete record.members[targetId];
271
470
  record.updatedAt = new Date().toISOString();
272
471
  writeRegistry(registry);
472
+ const selfRemoved = targetId === actorId;
473
+ const stillAllowed = prdWorkflowCollaborationAccess(record, actorId).allowed;
273
474
  return {
274
475
  workflow: publicWorkflow(record, actorId),
275
476
  removedUserId: targetId,
276
- left: targetId === actorId,
477
+ left: selfRemoved && !stillAllowed,
277
478
  };
278
479
  }