@fieldwangai/agentflow 0.1.131 → 0.1.134
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/bin/lib/prd-workflow-collaboration.mjs +24 -3
- package/bin/lib/teams.mjs +160 -0
- package/bin/lib/ui-server.mjs +193 -3
- package/bin/lib/workspace-collaboration.mjs +52 -2
- package/builtin/web-ui/dist/assets/index-CqXKONpd.js +350 -0
- package/builtin/web-ui/dist/assets/{index-DQA8LAlU.css → index-QDDFbZ_T.css} +1 -1
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-C1QUYyi5.js +0 -350
|
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
5
|
+
import { getTeamForUser } from "./teams.mjs";
|
|
5
6
|
|
|
6
7
|
const REGISTRY_VERSION = 1;
|
|
7
8
|
|
|
@@ -52,11 +53,14 @@ function publicWorkflow(record, userId = "") {
|
|
|
52
53
|
if (!record) return null;
|
|
53
54
|
const actorId = normalizeUserId(userId);
|
|
54
55
|
const members = record.members && typeof record.members === "object" ? record.members : {};
|
|
56
|
+
const access = prdWorkflowCollaborationAccess(record, actorId);
|
|
55
57
|
return {
|
|
56
58
|
id: record.id,
|
|
57
59
|
tapdId: record.tapdId,
|
|
58
60
|
ownerId: record.ownerId,
|
|
59
|
-
role:
|
|
61
|
+
role: access.role,
|
|
62
|
+
accessSource: access.source,
|
|
63
|
+
teamId: String(record.teamId || getTeamForUser(record.ownerId)?.id || ""),
|
|
60
64
|
memberCount: Object.keys(members).length,
|
|
61
65
|
members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
|
|
62
66
|
shareActive: Boolean(record.shareToken),
|
|
@@ -69,11 +73,17 @@ function publicWorkflow(record, userId = "") {
|
|
|
69
73
|
export function prdWorkflowCollaborationAccess(record, userId) {
|
|
70
74
|
if (!record) return { allowed: true, role: "" };
|
|
71
75
|
const actorId = normalizeUserId(userId);
|
|
72
|
-
const
|
|
76
|
+
const directRole = actorId === record.ownerId ? "owner" : String(record.members?.[actorId] || "");
|
|
77
|
+
const actorTeam = getTeamForUser(actorId);
|
|
78
|
+
const recordTeamId = String(record.teamId || getTeamForUser(record.ownerId)?.id || "");
|
|
79
|
+
const teamRole = actorTeam?.status === "active" && actorTeam.id === recordTeamId ? "viewer" : "";
|
|
80
|
+
const role = directRole || teamRole;
|
|
73
81
|
return {
|
|
74
82
|
allowed: role === "owner" || role === "editor" || role === "viewer",
|
|
75
83
|
writable: role === "owner" || role === "editor",
|
|
76
84
|
role,
|
|
85
|
+
source: directRole ? "member" : teamRole ? "team" : "",
|
|
86
|
+
teamId: teamRole ? recordTeamId : "",
|
|
77
87
|
};
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -109,7 +119,17 @@ export function listPrdWorkflowCollaborationsForUser(userId) {
|
|
|
109
119
|
const actorId = normalizeUserId(userId);
|
|
110
120
|
if (!actorId) return [];
|
|
111
121
|
return Object.values(readRegistry().workflows)
|
|
112
|
-
.filter((record) =>
|
|
122
|
+
.filter((record) => (
|
|
123
|
+
record?.ownerId === actorId || Boolean(record?.members?.[actorId])
|
|
124
|
+
))
|
|
125
|
+
.sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function listPrdWorkflowCollaborationsForTeam(teamId) {
|
|
129
|
+
const id = String(teamId || "").trim();
|
|
130
|
+
if (!id) return [];
|
|
131
|
+
return Object.values(readRegistry().workflows)
|
|
132
|
+
.filter((record) => String(record?.teamId || getTeamForUser(record?.ownerId)?.id || "") === id)
|
|
113
133
|
.sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
|
|
114
134
|
}
|
|
115
135
|
|
|
@@ -129,6 +149,7 @@ export function ensurePrdWorkflowCollaboration({ tapdId, userId }) {
|
|
|
129
149
|
id: workflowId,
|
|
130
150
|
tapdId: normalizedTapdId,
|
|
131
151
|
ownerId,
|
|
152
|
+
teamId: String(getTeamForUser(ownerId)?.id || ""),
|
|
132
153
|
members: { [ownerId]: "owner" },
|
|
133
154
|
createdAt: now,
|
|
134
155
|
updatedAt: now,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const REGISTRY_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
function registryPath() {
|
|
9
|
+
return path.join(getAgentflowDataRoot(), "organization", "teams.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeUserId(value) {
|
|
13
|
+
return String(value || "").trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeTeamId(value) {
|
|
17
|
+
return String(value || "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeMembers(value) {
|
|
21
|
+
const members = Array.isArray(value) ? value : [];
|
|
22
|
+
return Array.from(new Set(members.map(normalizeUserId).filter(Boolean)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readRegistry() {
|
|
26
|
+
try {
|
|
27
|
+
const filePath = registryPath();
|
|
28
|
+
if (!fs.existsSync(filePath)) return { version: REGISTRY_VERSION, teams: {} };
|
|
29
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
30
|
+
return {
|
|
31
|
+
version: REGISTRY_VERSION,
|
|
32
|
+
teams: parsed?.teams && typeof parsed.teams === "object" && !Array.isArray(parsed.teams)
|
|
33
|
+
? parsed.teams
|
|
34
|
+
: {},
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return { version: REGISTRY_VERSION, teams: {} };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeRegistry(registry) {
|
|
42
|
+
const filePath = registryPath();
|
|
43
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
44
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
45
|
+
fs.writeFileSync(tempPath, JSON.stringify({
|
|
46
|
+
version: REGISTRY_VERSION,
|
|
47
|
+
teams: registry?.teams || {},
|
|
48
|
+
}, null, 2) + "\n", "utf-8");
|
|
49
|
+
fs.renameSync(tempPath, filePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function publicTeam(record) {
|
|
53
|
+
if (!record) return null;
|
|
54
|
+
const members = normalizeMembers(record.members);
|
|
55
|
+
return {
|
|
56
|
+
id: String(record.id || ""),
|
|
57
|
+
name: String(record.name || ""),
|
|
58
|
+
description: String(record.description || ""),
|
|
59
|
+
status: record.status === "inactive" ? "inactive" : "active",
|
|
60
|
+
members,
|
|
61
|
+
memberCount: members.length,
|
|
62
|
+
createdAt: String(record.createdAt || ""),
|
|
63
|
+
updatedAt: String(record.updatedAt || ""),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function listTeams({ includeInactive = true } = {}) {
|
|
68
|
+
return Object.values(readRegistry().teams)
|
|
69
|
+
.map(publicTeam)
|
|
70
|
+
.filter((team) => includeInactive || team.status === "active")
|
|
71
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getTeamById(teamId) {
|
|
75
|
+
return publicTeam(readRegistry().teams[normalizeTeamId(teamId)] || null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getTeamForUser(userId, { includeInactive = false } = {}) {
|
|
79
|
+
const id = normalizeUserId(userId);
|
|
80
|
+
if (!id) return null;
|
|
81
|
+
return listTeams({ includeInactive }).find((team) => team.members.includes(id)) || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createTeam({ name, description = "" } = {}) {
|
|
85
|
+
const normalizedName = String(name || "").trim();
|
|
86
|
+
if (!normalizedName) return { error: "团队名称不能为空", status: 400 };
|
|
87
|
+
const registry = readRegistry();
|
|
88
|
+
const duplicate = Object.values(registry.teams).some((team) => (
|
|
89
|
+
String(team?.name || "").trim().toLowerCase() === normalizedName.toLowerCase()
|
|
90
|
+
));
|
|
91
|
+
if (duplicate) return { error: "已存在同名团队", status: 409 };
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const id = `team_${crypto.randomBytes(10).toString("hex")}`;
|
|
94
|
+
const record = {
|
|
95
|
+
id,
|
|
96
|
+
name: normalizedName,
|
|
97
|
+
description: String(description || "").trim(),
|
|
98
|
+
status: "active",
|
|
99
|
+
members: [],
|
|
100
|
+
createdAt: now,
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
};
|
|
103
|
+
registry.teams[id] = record;
|
|
104
|
+
writeRegistry(registry);
|
|
105
|
+
return { team: publicTeam(record), created: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function updateTeam(teamId, patch = {}) {
|
|
109
|
+
const id = normalizeTeamId(teamId);
|
|
110
|
+
const registry = readRegistry();
|
|
111
|
+
const record = registry.teams[id];
|
|
112
|
+
if (!record) return { error: "团队不存在", status: 404 };
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(patch, "name")) {
|
|
114
|
+
const name = String(patch.name || "").trim();
|
|
115
|
+
if (!name) return { error: "团队名称不能为空", status: 400 };
|
|
116
|
+
const duplicate = Object.values(registry.teams).some((team) => (
|
|
117
|
+
team?.id !== id && String(team?.name || "").trim().toLowerCase() === name.toLowerCase()
|
|
118
|
+
));
|
|
119
|
+
if (duplicate) return { error: "已存在同名团队", status: 409 };
|
|
120
|
+
record.name = name;
|
|
121
|
+
}
|
|
122
|
+
if (Object.prototype.hasOwnProperty.call(patch, "description")) {
|
|
123
|
+
record.description = String(patch.description || "").trim();
|
|
124
|
+
}
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(patch, "status")) {
|
|
126
|
+
record.status = patch.status === "inactive" ? "inactive" : "active";
|
|
127
|
+
}
|
|
128
|
+
record.updatedAt = new Date().toISOString();
|
|
129
|
+
writeRegistry(registry);
|
|
130
|
+
return { team: publicTeam(record) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function setTeamMembers(teamId, members = []) {
|
|
134
|
+
const id = normalizeTeamId(teamId);
|
|
135
|
+
const registry = readRegistry();
|
|
136
|
+
const record = registry.teams[id];
|
|
137
|
+
if (!record) return { error: "团队不存在", status: 404 };
|
|
138
|
+
const normalized = normalizeMembers(members);
|
|
139
|
+
for (const team of Object.values(registry.teams)) {
|
|
140
|
+
if (!team || team.id === id) continue;
|
|
141
|
+
team.members = normalizeMembers(team.members).filter((memberId) => !normalized.includes(memberId));
|
|
142
|
+
}
|
|
143
|
+
record.members = normalized;
|
|
144
|
+
record.updatedAt = new Date().toISOString();
|
|
145
|
+
writeRegistry(registry);
|
|
146
|
+
return { team: publicTeam(record) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function deleteTeam(teamId) {
|
|
150
|
+
const id = normalizeTeamId(teamId);
|
|
151
|
+
const registry = readRegistry();
|
|
152
|
+
const record = registry.teams[id];
|
|
153
|
+
if (!record) return { error: "团队不存在", status: 404 };
|
|
154
|
+
if (normalizeMembers(record.members).length > 0) {
|
|
155
|
+
return { error: "请先移出团队中的全部成员", status: 409 };
|
|
156
|
+
}
|
|
157
|
+
delete registry.teams[id];
|
|
158
|
+
writeRegistry(registry);
|
|
159
|
+
return { deleted: true, teamId: id };
|
|
160
|
+
}
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -145,6 +145,8 @@ import {
|
|
|
145
145
|
getWorkspaceCollaborationForProject,
|
|
146
146
|
listWorkspaceCollaborationsForUser,
|
|
147
147
|
removeWorkspaceCollaborationMember,
|
|
148
|
+
removeWorkspaceCollaborationTeamShare,
|
|
149
|
+
setWorkspaceCollaborationTeamShare,
|
|
148
150
|
updateWorkspaceCollaborationFlow,
|
|
149
151
|
workspaceCollaborationAccess,
|
|
150
152
|
workspaceCollaborationSummary,
|
|
@@ -157,11 +159,21 @@ import {
|
|
|
157
159
|
getPrdWorkflowCollaborationForUser,
|
|
158
160
|
ensurePrdWorkflowShareLink,
|
|
159
161
|
listPrdWorkflowCollaborationsForUser,
|
|
162
|
+
listPrdWorkflowCollaborationsForTeam,
|
|
160
163
|
prdWorkflowCollaborationAccess,
|
|
161
164
|
prdWorkflowCollaborationSummary,
|
|
162
165
|
removePrdWorkflowCollaborationMember,
|
|
163
166
|
revokePrdWorkflowShareLink,
|
|
164
167
|
} from "./prd-workflow-collaboration.mjs";
|
|
168
|
+
import {
|
|
169
|
+
createTeam,
|
|
170
|
+
deleteTeam,
|
|
171
|
+
getTeamById,
|
|
172
|
+
getTeamForUser,
|
|
173
|
+
listTeams,
|
|
174
|
+
setTeamMembers,
|
|
175
|
+
updateTeam,
|
|
176
|
+
} from "./teams.mjs";
|
|
165
177
|
import {
|
|
166
178
|
legacyOverallToGlobalState,
|
|
167
179
|
materializeWorkflowGlobalState,
|
|
@@ -3305,6 +3317,7 @@ function workspaceCollaborationSummaryWithUsers(record, userId) {
|
|
|
3305
3317
|
const summary = workspaceCollaborationSummary(record, userId);
|
|
3306
3318
|
if (!summary) return null;
|
|
3307
3319
|
const users = readAuthUsers();
|
|
3320
|
+
const teams = new Map(listTeams().map((team) => [team.id, team]));
|
|
3308
3321
|
return {
|
|
3309
3322
|
...summary,
|
|
3310
3323
|
ownerUsername: String(users[summary.ownerId]?.username || summary.ownerId),
|
|
@@ -3312,6 +3325,23 @@ function workspaceCollaborationSummaryWithUsers(record, userId) {
|
|
|
3312
3325
|
...member,
|
|
3313
3326
|
username: String(users[member.userId]?.username || member.userId),
|
|
3314
3327
|
})),
|
|
3328
|
+
teamShares: (summary.teamShares || []).map((share) => ({
|
|
3329
|
+
...share,
|
|
3330
|
+
teamName: String(teams.get(share.teamId)?.name || share.teamId),
|
|
3331
|
+
})),
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
function teamSummaryWithUsers(team) {
|
|
3336
|
+
if (!team) return null;
|
|
3337
|
+
const users = readAuthUsers();
|
|
3338
|
+
return {
|
|
3339
|
+
...team,
|
|
3340
|
+
members: (team.members || []).map((userId) => ({
|
|
3341
|
+
userId,
|
|
3342
|
+
username: String(users[userId]?.username || userId),
|
|
3343
|
+
isAdmin: Boolean(users[userId]?.isAdmin),
|
|
3344
|
+
})),
|
|
3315
3345
|
};
|
|
3316
3346
|
}
|
|
3317
3347
|
|
|
@@ -3462,6 +3492,9 @@ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
|
|
|
3462
3492
|
ownerId: String(collaboration.ownerId || ""),
|
|
3463
3493
|
ownerUsername: String(collaboration.ownerUsername || collaboration.ownerId || ""),
|
|
3464
3494
|
memberCount: Number(collaboration.memberCount || 0),
|
|
3495
|
+
accessSource: String(collaboration.accessSource || ""),
|
|
3496
|
+
teamId: String(collaboration.teamId || ""),
|
|
3497
|
+
teamName: String(getTeamById(collaboration.teamId)?.name || ""),
|
|
3465
3498
|
shareActive: collaboration.shareActive === true,
|
|
3466
3499
|
updatedAt: updatedAtTimestamp ? new Date(updatedAtTimestamp).toISOString() : String(record?.updatedAt || ""),
|
|
3467
3500
|
};
|
|
@@ -12473,7 +12506,23 @@ export function startUiServer({
|
|
|
12473
12506
|
return;
|
|
12474
12507
|
}
|
|
12475
12508
|
try {
|
|
12476
|
-
const
|
|
12509
|
+
const view = String(url.searchParams.get("view") || "personal").trim().toLowerCase();
|
|
12510
|
+
let team = null;
|
|
12511
|
+
let records;
|
|
12512
|
+
if (view === "team") {
|
|
12513
|
+
const requestedTeamId = String(url.searchParams.get("teamId") || "").trim();
|
|
12514
|
+
team = requestedTeamId && authUser?.isAdmin
|
|
12515
|
+
? getTeamById(requestedTeamId)
|
|
12516
|
+
: getTeamForUser(userCtx.userId);
|
|
12517
|
+
if (!team || team.status !== "active") {
|
|
12518
|
+
json(res, 200, { ok: true, view: "team", team: null, workflows: [] });
|
|
12519
|
+
return;
|
|
12520
|
+
}
|
|
12521
|
+
records = listPrdWorkflowCollaborationsForTeam(team.id);
|
|
12522
|
+
} else {
|
|
12523
|
+
records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
|
|
12524
|
+
}
|
|
12525
|
+
const workflows = records.map((record) => {
|
|
12477
12526
|
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
|
|
12478
12527
|
const tapdId = String(record.tapdId || "").trim();
|
|
12479
12528
|
const project = prdWorkflowReadProjectState(stateRoot, tapdId);
|
|
@@ -12482,7 +12531,7 @@ export function startUiServer({
|
|
|
12482
12531
|
const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
|
|
12483
12532
|
return prdWorkflowDashboardSummary(record, snapshot, userCtx);
|
|
12484
12533
|
});
|
|
12485
|
-
json(res, 200, { ok: true, workflows });
|
|
12534
|
+
json(res, 200, { ok: true, view: view === "team" ? "team" : "personal", team: teamSummaryWithUsers(team), workflows });
|
|
12486
12535
|
} catch (error) {
|
|
12487
12536
|
json(res, 500, { error: (error && error.message) || String(error) });
|
|
12488
12537
|
}
|
|
@@ -14158,6 +14207,62 @@ export function startUiServer({
|
|
|
14158
14207
|
return;
|
|
14159
14208
|
}
|
|
14160
14209
|
|
|
14210
|
+
if (req.method === "GET" && url.pathname === "/api/teams/me") {
|
|
14211
|
+
const team = getTeamForUser(userCtx.userId);
|
|
14212
|
+
json(res, 200, { team: teamSummaryWithUsers(team) });
|
|
14213
|
+
return;
|
|
14214
|
+
}
|
|
14215
|
+
|
|
14216
|
+
if (url.pathname === "/api/admin/teams") {
|
|
14217
|
+
if (!authUser?.isAdmin) {
|
|
14218
|
+
json(res, 403, { error: "Admin permission required" });
|
|
14219
|
+
return;
|
|
14220
|
+
}
|
|
14221
|
+
if (req.method === "GET") {
|
|
14222
|
+
const assigned = new Set(listTeams().flatMap((team) => team.members || []));
|
|
14223
|
+
const users = Object.entries(readAuthUsers()).map(([userId, user]) => ({
|
|
14224
|
+
userId,
|
|
14225
|
+
username: String(user?.username || userId),
|
|
14226
|
+
isAdmin: Boolean(user?.isAdmin),
|
|
14227
|
+
assigned: assigned.has(userId),
|
|
14228
|
+
teamId: getTeamForUser(userId, { includeInactive: true })?.id || "",
|
|
14229
|
+
}));
|
|
14230
|
+
json(res, 200, { teams: listTeams().map(teamSummaryWithUsers), users });
|
|
14231
|
+
return;
|
|
14232
|
+
}
|
|
14233
|
+
let payload;
|
|
14234
|
+
try {
|
|
14235
|
+
payload = JSON.parse(await readBody(req));
|
|
14236
|
+
} catch {
|
|
14237
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
14238
|
+
return;
|
|
14239
|
+
}
|
|
14240
|
+
let result;
|
|
14241
|
+
if (req.method === "POST") {
|
|
14242
|
+
result = createTeam(payload || {});
|
|
14243
|
+
} else if (req.method === "PATCH") {
|
|
14244
|
+
result = updateTeam(payload?.teamId, payload || {});
|
|
14245
|
+
} else if (req.method === "PUT") {
|
|
14246
|
+
const users = readAuthUsers();
|
|
14247
|
+
const memberIds = Array.isArray(payload?.members) ? payload.members.map((value) => String(value || "").trim().toLowerCase()) : [];
|
|
14248
|
+
const unknown = memberIds.find((userId) => !users[userId]);
|
|
14249
|
+
result = unknown
|
|
14250
|
+
? { error: `用户不存在:${unknown}`, status: 404 }
|
|
14251
|
+
: setTeamMembers(payload?.teamId, memberIds);
|
|
14252
|
+
} else if (req.method === "DELETE") {
|
|
14253
|
+
result = deleteTeam(payload?.teamId);
|
|
14254
|
+
} else {
|
|
14255
|
+
json(res, 405, { error: "Method not allowed" });
|
|
14256
|
+
return;
|
|
14257
|
+
}
|
|
14258
|
+
if (result?.error) {
|
|
14259
|
+
json(res, result.status || 400, { error: result.error });
|
|
14260
|
+
return;
|
|
14261
|
+
}
|
|
14262
|
+
json(res, 200, { ok: true, ...result, teams: listTeams().map(teamSummaryWithUsers) });
|
|
14263
|
+
return;
|
|
14264
|
+
}
|
|
14265
|
+
|
|
14161
14266
|
if (url.pathname === "/api/feedback") {
|
|
14162
14267
|
if (req.method === "POST") {
|
|
14163
14268
|
let payload;
|
|
@@ -14345,6 +14450,8 @@ export function startUiServer({
|
|
|
14345
14450
|
if (url.pathname === "/api/flows") {
|
|
14346
14451
|
if (req.method === "GET") {
|
|
14347
14452
|
try {
|
|
14453
|
+
const projectView = String(url.searchParams.get("view") || "all").trim().toLowerCase();
|
|
14454
|
+
const currentTeam = getTeamForUser(userCtx.userId);
|
|
14348
14455
|
const flows = listFlowsJson(root, { ...userCtx, includeWorkspaceFlows: true })
|
|
14349
14456
|
.filter((flow) => (
|
|
14350
14457
|
!workspaceFlowCollaborationGuard(
|
|
@@ -14387,7 +14494,22 @@ export function startUiServer({
|
|
|
14387
14494
|
});
|
|
14388
14495
|
existingCollaborationIds.add(record.id);
|
|
14389
14496
|
}
|
|
14390
|
-
|
|
14497
|
+
const visibleFlows = projectView === "team"
|
|
14498
|
+
? flows.filter((flow) => (
|
|
14499
|
+
currentTeam
|
|
14500
|
+
&& Array.isArray(flow.collaboration?.teamShares)
|
|
14501
|
+
&& flow.collaboration.teamShares.some((share) => share.teamId === currentTeam.id)
|
|
14502
|
+
))
|
|
14503
|
+
: projectView === "personal"
|
|
14504
|
+
? flows.filter((flow) => (
|
|
14505
|
+
!flow.collaboration
|
|
14506
|
+
|| flow.collaboration.ownerId === userCtx.userId
|
|
14507
|
+
|| flow.collaboration.members?.some((member) => member.userId === userCtx.userId)
|
|
14508
|
+
|| flow.source === "builtin"
|
|
14509
|
+
|| flow.source === "admin"
|
|
14510
|
+
))
|
|
14511
|
+
: flows;
|
|
14512
|
+
json(res, 200, visibleFlows);
|
|
14391
14513
|
} catch (e) {
|
|
14392
14514
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
14393
14515
|
}
|
|
@@ -14714,6 +14836,74 @@ export function startUiServer({
|
|
|
14714
14836
|
return;
|
|
14715
14837
|
}
|
|
14716
14838
|
|
|
14839
|
+
if (url.pathname === "/api/workspace/collaboration/team-share" && (req.method === "POST" || req.method === "DELETE")) {
|
|
14840
|
+
try {
|
|
14841
|
+
const payload = JSON.parse(await readBody(req));
|
|
14842
|
+
const flowId = String(payload?.flowId || "").trim();
|
|
14843
|
+
const flowSource = String(payload?.flowSource || "user").trim();
|
|
14844
|
+
const archived = payload?.archived === true || payload?.flowArchived === true;
|
|
14845
|
+
if (!flowId || (flowSource !== "workspace" && flowSource !== "user")) {
|
|
14846
|
+
json(res, 400, { error: "当前 Project 不支持团队分享" });
|
|
14847
|
+
return;
|
|
14848
|
+
}
|
|
14849
|
+
const targetTeam = getTeamById(payload?.teamId);
|
|
14850
|
+
const actorTeam = getTeamForUser(userCtx.userId);
|
|
14851
|
+
if (!targetTeam || targetTeam.status !== "active") {
|
|
14852
|
+
json(res, 404, { error: "团队不存在或已停用" });
|
|
14853
|
+
return;
|
|
14854
|
+
}
|
|
14855
|
+
if (!authUser?.isAdmin && actorTeam?.id !== targetTeam.id) {
|
|
14856
|
+
json(res, 403, { error: "只能分享给自己所在的团队" });
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14859
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
14860
|
+
flowId,
|
|
14861
|
+
flowSource,
|
|
14862
|
+
workspaceId: payload.workspaceId || "",
|
|
14863
|
+
archived,
|
|
14864
|
+
}, userCtx);
|
|
14865
|
+
if (scoped.error) {
|
|
14866
|
+
json(res, scoped.status || 400, { error: scoped.error });
|
|
14867
|
+
return;
|
|
14868
|
+
}
|
|
14869
|
+
const ensured = ensureWorkspaceCollaboration({
|
|
14870
|
+
flowId,
|
|
14871
|
+
flowSource,
|
|
14872
|
+
archived,
|
|
14873
|
+
userId: userCtx.userId,
|
|
14874
|
+
});
|
|
14875
|
+
if (ensured.error) {
|
|
14876
|
+
json(res, ensured.status || 400, { error: ensured.error });
|
|
14877
|
+
return;
|
|
14878
|
+
}
|
|
14879
|
+
const result = req.method === "POST"
|
|
14880
|
+
? setWorkspaceCollaborationTeamShare({
|
|
14881
|
+
workspaceId: ensured.workspace.id,
|
|
14882
|
+
userId: userCtx.userId,
|
|
14883
|
+
teamId: targetTeam.id,
|
|
14884
|
+
role: payload?.role,
|
|
14885
|
+
})
|
|
14886
|
+
: removeWorkspaceCollaborationTeamShare({
|
|
14887
|
+
workspaceId: ensured.workspace.id,
|
|
14888
|
+
userId: userCtx.userId,
|
|
14889
|
+
teamId: targetTeam.id,
|
|
14890
|
+
});
|
|
14891
|
+
if (result.error) {
|
|
14892
|
+
json(res, result.status || 400, { error: result.error });
|
|
14893
|
+
return;
|
|
14894
|
+
}
|
|
14895
|
+
const record = getWorkspaceCollaborationForProject({ workspaceId: ensured.workspace.id });
|
|
14896
|
+
json(res, 200, {
|
|
14897
|
+
ok: true,
|
|
14898
|
+
workspace: workspaceCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
14899
|
+
team: teamSummaryWithUsers(targetTeam),
|
|
14900
|
+
});
|
|
14901
|
+
} catch (e) {
|
|
14902
|
+
json(res, 400, { error: (e && e.message) || String(e) });
|
|
14903
|
+
}
|
|
14904
|
+
return;
|
|
14905
|
+
}
|
|
14906
|
+
|
|
14717
14907
|
if (req.method === "DELETE" && url.pathname === "/api/workspace/collaboration/share") {
|
|
14718
14908
|
try {
|
|
14719
14909
|
const payload = JSON.parse(await readBody(req));
|
|
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
5
|
+
import { getTeamForUser } from "./teams.mjs";
|
|
5
6
|
|
|
6
7
|
const REGISTRY_VERSION = 1;
|
|
7
8
|
const DEFAULT_INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -67,15 +68,19 @@ function publicWorkspace(record, userId = "") {
|
|
|
67
68
|
if (!record) return null;
|
|
68
69
|
const actorId = normalizeUserId(userId);
|
|
69
70
|
const members = record.members && typeof record.members === "object" ? record.members : {};
|
|
71
|
+
const access = workspaceCollaborationAccess(record, actorId);
|
|
70
72
|
return {
|
|
71
73
|
id: record.id,
|
|
72
74
|
flowId: record.flowId,
|
|
73
75
|
flowSource: record.projectSource || record.flowSource || "workspace",
|
|
74
76
|
archived: record.archived === true,
|
|
75
77
|
ownerId: record.ownerId,
|
|
76
|
-
role:
|
|
78
|
+
role: access.role,
|
|
79
|
+
accessSource: access.source,
|
|
80
|
+
teamId: access.teamId,
|
|
77
81
|
memberCount: Object.keys(members).length,
|
|
78
82
|
members: Object.entries(members).map(([id, role]) => ({ userId: id, role })),
|
|
83
|
+
teamShares: Object.entries(record.teamShares || {}).map(([teamId, role]) => ({ teamId, role })),
|
|
79
84
|
createdAt: record.createdAt || "",
|
|
80
85
|
updatedAt: record.updatedAt || "",
|
|
81
86
|
};
|
|
@@ -98,12 +103,19 @@ export function getWorkspaceCollaborationById(workspaceId) {
|
|
|
98
103
|
export function workspaceCollaborationAccess(record, userId) {
|
|
99
104
|
if (!record) return { allowed: true, role: "" };
|
|
100
105
|
const id = normalizeUserId(userId);
|
|
101
|
-
const
|
|
106
|
+
const directRole = id === record.ownerId ? "owner" : String(record.members?.[id] || "");
|
|
107
|
+
const team = getTeamForUser(id);
|
|
108
|
+
const teamRole = team ? String(record.teamShares?.[team.id] || "") : "";
|
|
109
|
+
const role = directRole === "owner" || directRole === "editor" || teamRole === "editor"
|
|
110
|
+
? directRole === "owner" ? "owner" : "editor"
|
|
111
|
+
: directRole === "viewer" || teamRole === "viewer" ? "viewer" : "";
|
|
102
112
|
return {
|
|
103
113
|
allowed: role === "owner" || role === "editor" || role === "viewer",
|
|
104
114
|
writable: role === "owner" || role === "editor",
|
|
105
115
|
runnable: role === "owner" || role === "editor",
|
|
106
116
|
role,
|
|
117
|
+
source: directRole ? "member" : teamRole ? "team" : "",
|
|
118
|
+
teamId: teamRole ? team?.id || "" : "",
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
|
|
@@ -256,6 +268,44 @@ export function addWorkspaceCollaborationMember({
|
|
|
256
268
|
return { workspace: publicWorkspace(record, actorId), memberUserId: targetId };
|
|
257
269
|
}
|
|
258
270
|
|
|
271
|
+
export function setWorkspaceCollaborationTeamShare({
|
|
272
|
+
workspaceId,
|
|
273
|
+
userId,
|
|
274
|
+
teamId,
|
|
275
|
+
role = "viewer",
|
|
276
|
+
}) {
|
|
277
|
+
const registry = readRegistry();
|
|
278
|
+
const record = registry.workspaces[String(workspaceId || "").trim()];
|
|
279
|
+
if (!record) return { error: "Workspace collaboration not found", status: 404 };
|
|
280
|
+
const actorId = normalizeUserId(userId);
|
|
281
|
+
const targetTeamId = String(teamId || "").trim();
|
|
282
|
+
if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
|
|
283
|
+
return { error: "Only the workspace owner can share with a team", status: 403 };
|
|
284
|
+
}
|
|
285
|
+
if (!targetTeamId) return { error: "Missing teamId", status: 400 };
|
|
286
|
+
record.teamShares = record.teamShares && typeof record.teamShares === "object" ? record.teamShares : {};
|
|
287
|
+
record.teamShares[targetTeamId] = role === "editor" ? "editor" : "viewer";
|
|
288
|
+
record.updatedAt = new Date().toISOString();
|
|
289
|
+
writeRegistry(registry);
|
|
290
|
+
return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function removeWorkspaceCollaborationTeamShare({ workspaceId, userId, teamId }) {
|
|
294
|
+
const registry = readRegistry();
|
|
295
|
+
const record = registry.workspaces[String(workspaceId || "").trim()];
|
|
296
|
+
if (!record) return { error: "Workspace collaboration not found", status: 404 };
|
|
297
|
+
const actorId = normalizeUserId(userId);
|
|
298
|
+
const targetTeamId = String(teamId || "").trim();
|
|
299
|
+
if (workspaceCollaborationAccess(record, actorId).role !== "owner") {
|
|
300
|
+
return { error: "Only the workspace owner can revoke a team share", status: 403 };
|
|
301
|
+
}
|
|
302
|
+
const removed = Boolean(record.teamShares?.[targetTeamId]);
|
|
303
|
+
if (removed) delete record.teamShares[targetTeamId];
|
|
304
|
+
record.updatedAt = new Date().toISOString();
|
|
305
|
+
writeRegistry(registry);
|
|
306
|
+
return { workspace: publicWorkspace(record, actorId), teamId: targetTeamId, removed };
|
|
307
|
+
}
|
|
308
|
+
|
|
259
309
|
export function removeWorkspaceCollaborationMember({
|
|
260
310
|
workspaceId,
|
|
261
311
|
userId,
|