oasis_test_v2 2.2.80 → 2.2.82
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/index.js +1241 -92
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2950,6 +2950,25 @@ async function fetchResultPackage(base, fetchImpl, runId) {
|
|
|
2950
2950
|
}));
|
|
2951
2951
|
return { artifacts, fileCount: body2.project?.fileCount ?? artifacts.length };
|
|
2952
2952
|
}
|
|
2953
|
+
function toEpochMs(v2) {
|
|
2954
|
+
if (typeof v2 === "number" && Number.isFinite(v2)) return v2;
|
|
2955
|
+
if (typeof v2 === "string" && v2.length > 0) {
|
|
2956
|
+
const ms = Date.parse(v2);
|
|
2957
|
+
return Number.isNaN(ms) ? void 0 : ms;
|
|
2958
|
+
}
|
|
2959
|
+
return void 0;
|
|
2960
|
+
}
|
|
2961
|
+
function runTime(r) {
|
|
2962
|
+
return r.createdAt ?? r.updatedAt ?? Number.NEGATIVE_INFINITY;
|
|
2963
|
+
}
|
|
2964
|
+
function latestSucceededRun(runs) {
|
|
2965
|
+
let best;
|
|
2966
|
+
for (const r of runs) {
|
|
2967
|
+
if (r.status !== "succeeded") continue;
|
|
2968
|
+
if (best === void 0 || runTime(r) > runTime(best)) best = r;
|
|
2969
|
+
}
|
|
2970
|
+
return best;
|
|
2971
|
+
}
|
|
2953
2972
|
async function fetchBoardStatus(base, fetchImpl, artifactId) {
|
|
2954
2973
|
const boardKey = deriveBoardKey(artifactId);
|
|
2955
2974
|
let boardExists = true;
|
|
@@ -2975,10 +2994,20 @@ async function fetchBoardStatus(base, fetchImpl, artifactId) {
|
|
|
2975
2994
|
"GET",
|
|
2976
2995
|
`/api/runs?projectId=${encodeURIComponent(boardKey)}`
|
|
2977
2996
|
);
|
|
2978
|
-
runs = (body2.runs ?? []).map((r) =>
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2997
|
+
runs = (body2.runs ?? []).map((r) => {
|
|
2998
|
+
const run = {
|
|
2999
|
+
runId: String(r["runId"] ?? r["id"] ?? ""),
|
|
3000
|
+
status: String(r["status"] ?? "unknown")
|
|
3001
|
+
};
|
|
3002
|
+
if (typeof r["agentId"] === "string" && r["agentId"].length > 0) run.agentId = r["agentId"];
|
|
3003
|
+
if (typeof r["error"] === "string" && r["error"].trim().length > 0) run.error = r["error"];
|
|
3004
|
+
const createdAt = toEpochMs(r["createdAt"]);
|
|
3005
|
+
if (createdAt !== void 0) run.createdAt = createdAt;
|
|
3006
|
+
const updatedAt = toEpochMs(r["updatedAt"]);
|
|
3007
|
+
if (updatedAt !== void 0) run.updatedAt = updatedAt;
|
|
3008
|
+
return run;
|
|
3009
|
+
});
|
|
3010
|
+
runs.sort((a, b2) => runTime(b2) - runTime(a) || 0);
|
|
2982
3011
|
} catch {
|
|
2983
3012
|
}
|
|
2984
3013
|
}
|
|
@@ -218354,13 +218383,22 @@ function cloneOffboarding(op) {
|
|
|
218354
218383
|
assignmentDecisions: op.assignmentDecisions.map((d) => ({ ...d, toPrincipal: { ...d.toPrincipal } }))
|
|
218355
218384
|
};
|
|
218356
218385
|
}
|
|
218357
|
-
var memberKey, scopedKey, bridgeKey, MemoryControlPlaneStore;
|
|
218386
|
+
var memberKey, scopedKey, bridgeKey, migrationRow, MemoryControlPlaneStore;
|
|
218358
218387
|
var init_memory_control_plane_store = __esm({
|
|
218359
218388
|
"../testkit/src/memory-control-plane-store.ts"() {
|
|
218360
218389
|
"use strict";
|
|
218361
218390
|
memberKey = (companyId, accountId) => `${companyId} ${accountId}`;
|
|
218362
218391
|
scopedKey = (companyId, id) => `${companyId}\0${id}`;
|
|
218363
218392
|
bridgeKey = (companyId, type, id) => `${companyId}\0${type}\0${id}`;
|
|
218393
|
+
migrationRow = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
|
|
218394
|
+
companyId,
|
|
218395
|
+
accountId,
|
|
218396
|
+
roleBefore,
|
|
218397
|
+
roleAfter,
|
|
218398
|
+
evidence: audit2.evidence,
|
|
218399
|
+
...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
|
|
218400
|
+
appliedAt: audit2.appliedAt
|
|
218401
|
+
});
|
|
218364
218402
|
MemoryControlPlaneStore = class {
|
|
218365
218403
|
companies = /* @__PURE__ */ new Map();
|
|
218366
218404
|
members = /* @__PURE__ */ new Map();
|
|
@@ -218387,6 +218425,8 @@ var init_memory_control_plane_store = __esm({
|
|
|
218387
218425
|
// key = companyId\0type\0id
|
|
218388
218426
|
offboardings = /* @__PURE__ */ new Map();
|
|
218389
218427
|
// key = companyId\0operationId
|
|
218428
|
+
/** 存量身份映射审计(ADR D3.4):**只追加**的一条数组,插入序即写入序。 */
|
|
218429
|
+
roleMigrations = [];
|
|
218390
218430
|
// —— 公司 ——
|
|
218391
218431
|
async createCompany(c) {
|
|
218392
218432
|
if (this.companies.has(c.id)) throw new Error(`company already exists: ${c.id}`);
|
|
@@ -218418,8 +218458,25 @@ var init_memory_control_plane_store = __esm({
|
|
|
218418
218458
|
}
|
|
218419
218459
|
if (patch.status !== void 0) c.status = patch.status;
|
|
218420
218460
|
if (patch.onboardingVariant !== void 0) c.onboardingVariant = patch.onboardingVariant;
|
|
218461
|
+
if (patch.dissolvedAt !== void 0) {
|
|
218462
|
+
if (patch.dissolvedAt === null) delete c.dissolvedAt;
|
|
218463
|
+
else c.dissolvedAt = patch.dissolvedAt;
|
|
218464
|
+
}
|
|
218465
|
+
if (patch.dissolvedBy !== void 0) {
|
|
218466
|
+
if (patch.dissolvedBy === null) delete c.dissolvedBy;
|
|
218467
|
+
else c.dissolvedBy = patch.dissolvedBy;
|
|
218468
|
+
}
|
|
218421
218469
|
this.companies.set(id, c);
|
|
218422
218470
|
}
|
|
218471
|
+
async dissolveCompanyIfOwner(companyId, accountId, at2) {
|
|
218472
|
+
const c = this.companies.get(companyId);
|
|
218473
|
+
if (!c) return { ok: false, reason: "not_found" };
|
|
218474
|
+
const caller = this.members.get(memberKey(companyId, accountId));
|
|
218475
|
+
if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
|
|
218476
|
+
const next = { ...c, status: "suspended", dissolvedAt: at2, dissolvedBy: accountId };
|
|
218477
|
+
this.companies.set(companyId, next);
|
|
218478
|
+
return { ok: true, company: { ...next } };
|
|
218479
|
+
}
|
|
218423
218480
|
// —— 成员 ——
|
|
218424
218481
|
async upsertMember(m2) {
|
|
218425
218482
|
const key2 = memberKey(m2.companyId, m2.accountId);
|
|
@@ -218461,6 +218518,21 @@ var init_memory_control_plane_store = __esm({
|
|
|
218461
218518
|
this.members.delete(key2);
|
|
218462
218519
|
return { ok: true, member: { ...target } };
|
|
218463
218520
|
}
|
|
218521
|
+
async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
|
|
218522
|
+
const target = this.members.get(memberKey(companyId, accountId));
|
|
218523
|
+
if (!target) return { ok: false, reason: "not_found" };
|
|
218524
|
+
if (target.role === "owner") return { ok: false, reason: "owner_protected" };
|
|
218525
|
+
const releasedAgents = [];
|
|
218526
|
+
for (const [key2, e] of this.digitalEmployees) {
|
|
218527
|
+
if (e.companyId !== companyId || e.ownerAccountId !== accountId) continue;
|
|
218528
|
+
const { ownerAccountId: _cleared, ...rest } = e;
|
|
218529
|
+
this.digitalEmployees.set(key2, { ...rest, updatedAt: at2 });
|
|
218530
|
+
releasedAgents.push(e.id);
|
|
218531
|
+
}
|
|
218532
|
+
this.principalBridges.delete(bridgeKey(companyId, "human", accountId));
|
|
218533
|
+
this.members.delete(memberKey(companyId, accountId));
|
|
218534
|
+
return { ok: true, member: { ...target }, releasedAgents };
|
|
218535
|
+
}
|
|
218464
218536
|
async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
|
|
218465
218537
|
const key2 = memberKey(companyId, accountId);
|
|
218466
218538
|
const target = this.members.get(key2);
|
|
@@ -218476,6 +218548,72 @@ var init_memory_control_plane_store = __esm({
|
|
|
218476
218548
|
this.members.set(key2, updated);
|
|
218477
218549
|
return { ok: true, member: { ...updated } };
|
|
218478
218550
|
}
|
|
218551
|
+
async setSoleCompanyOwner(companyId, toAccountId, opts) {
|
|
218552
|
+
const target = this.members.get(memberKey(companyId, toAccountId));
|
|
218553
|
+
if (!target) return { ok: false, reason: "not_found" };
|
|
218554
|
+
if (opts?.requireCurrentOwner !== void 0) {
|
|
218555
|
+
const caller = this.members.get(memberKey(companyId, opts.requireCurrentOwner));
|
|
218556
|
+
if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
|
|
218557
|
+
}
|
|
218558
|
+
if (!target.onboardingCompletedAt && target.role !== "owner") {
|
|
218559
|
+
return { ok: false, reason: "target_not_listed" };
|
|
218560
|
+
}
|
|
218561
|
+
const acc = this.accounts.get(toAccountId);
|
|
218562
|
+
if (!acc || acc.status !== "active") return { ok: false, reason: "target_inactive" };
|
|
218563
|
+
const others = [];
|
|
218564
|
+
for (const m2 of this.members.values()) {
|
|
218565
|
+
if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
|
|
218566
|
+
}
|
|
218567
|
+
if (others.length === 0 && target.role === "owner") {
|
|
218568
|
+
return { ok: true, owner: { ...target }, demoted: [] };
|
|
218569
|
+
}
|
|
218570
|
+
const demoted = others.map((m2) => {
|
|
218571
|
+
const next = { ...m2, role: "admin" };
|
|
218572
|
+
this.members.set(memberKey(companyId, m2.accountId), next);
|
|
218573
|
+
return { ...next };
|
|
218574
|
+
});
|
|
218575
|
+
const owner = { ...target, role: "owner" };
|
|
218576
|
+
this.members.set(memberKey(companyId, toAccountId), owner);
|
|
218577
|
+
return { ok: true, owner: { ...owner }, demoted };
|
|
218578
|
+
}
|
|
218579
|
+
// —— 存量身份映射(ADR D3)——
|
|
218580
|
+
async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
|
|
218581
|
+
const target = this.members.get(memberKey(companyId, toAccountId));
|
|
218582
|
+
if (!target) return { ok: false, reason: "not_found" };
|
|
218583
|
+
const others = [];
|
|
218584
|
+
for (const m2 of this.members.values()) {
|
|
218585
|
+
if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
|
|
218586
|
+
}
|
|
218587
|
+
if (others.length === 0 && target.role === "owner") {
|
|
218588
|
+
return { ok: true, owner: { ...target }, demoted: [], auditRows: [] };
|
|
218589
|
+
}
|
|
218590
|
+
const auditRows = [];
|
|
218591
|
+
const demoted = others.map((m2) => {
|
|
218592
|
+
const next = { ...m2, role: "admin" };
|
|
218593
|
+
this.members.set(memberKey(companyId, m2.accountId), next);
|
|
218594
|
+
auditRows.push(migrationRow(companyId, m2.accountId, "owner", "admin", audit2));
|
|
218595
|
+
return { ...next };
|
|
218596
|
+
});
|
|
218597
|
+
if (target.role !== "owner") auditRows.push(migrationRow(companyId, toAccountId, target.role, "owner", audit2));
|
|
218598
|
+
const owner = { ...target, role: "owner" };
|
|
218599
|
+
this.members.set(memberKey(companyId, toAccountId), owner);
|
|
218600
|
+
for (const r of auditRows) this.roleMigrations.push({ ...r });
|
|
218601
|
+
return { ok: true, owner: { ...owner }, demoted, auditRows };
|
|
218602
|
+
}
|
|
218603
|
+
async setMemberRoleOnly(companyId, accountId, role) {
|
|
218604
|
+
const key2 = memberKey(companyId, accountId);
|
|
218605
|
+
const cur = this.members.get(key2);
|
|
218606
|
+
if (!cur) return false;
|
|
218607
|
+
this.members.set(key2, { ...cur, role });
|
|
218608
|
+
return true;
|
|
218609
|
+
}
|
|
218610
|
+
// —— 存量身份映射审计(ADR D3.4):只追加 ——
|
|
218611
|
+
async appendMemberRoleMigrations(rows) {
|
|
218612
|
+
for (const r of rows) this.roleMigrations.push({ ...r });
|
|
218613
|
+
}
|
|
218614
|
+
async listMemberRoleMigrations(companyId) {
|
|
218615
|
+
return this.roleMigrations.filter((r) => companyId === void 0 || r.companyId === companyId).map((r) => ({ ...r }));
|
|
218616
|
+
}
|
|
218479
218617
|
// —— 邀请 ——
|
|
218480
218618
|
async createInvitation(inv) {
|
|
218481
218619
|
if (this.invitations.has(inv.id)) throw new Error(`invitation already exists: ${inv.id}`);
|
|
@@ -223753,9 +223891,16 @@ var init_control_plane_file_store = __esm({
|
|
|
223753
223891
|
MUTATORS2 = [
|
|
223754
223892
|
"createCompany",
|
|
223755
223893
|
"updateCompany",
|
|
223894
|
+
"dissolveCompanyIfOwner",
|
|
223756
223895
|
"upsertMember",
|
|
223757
223896
|
"removeMember",
|
|
223758
223897
|
"removeMemberIfNotLastOwner",
|
|
223898
|
+
"updateMemberRoleIfNotLastOwner",
|
|
223899
|
+
"setSoleCompanyOwner",
|
|
223900
|
+
"removeMemberAndReleaseOwnedAgents",
|
|
223901
|
+
"migrateSoleCompanyOwner",
|
|
223902
|
+
"setMemberRoleOnly",
|
|
223903
|
+
"appendMemberRoleMigrations",
|
|
223759
223904
|
"createInvitation",
|
|
223760
223905
|
"updateInvitationStatus",
|
|
223761
223906
|
"updateInvitationStatusIf",
|
|
@@ -223807,6 +223952,7 @@ var init_control_plane_file_store = __esm({
|
|
|
223807
223952
|
for (const c of snap.agentCredentials ?? []) await MemoryControlPlaneStore.prototype.putAgentCredential.call(store, c);
|
|
223808
223953
|
for (const b2 of snap.principalBridges ?? []) await MemoryControlPlaneStore.prototype.putPrincipalBridge.call(store, b2);
|
|
223809
223954
|
for (const op of snap.offboardings ?? []) store.offboardings.set(`${op.companyId}\0${op.operationId}`, op);
|
|
223955
|
+
for (const r of snap.roleMigrations ?? []) store.roleMigrations.push({ ...r });
|
|
223810
223956
|
if (needsOnboardingBackfill) await store.save();
|
|
223811
223957
|
} else {
|
|
223812
223958
|
fs25.mkdirSync(path24.dirname(file), { recursive: true });
|
|
@@ -223844,7 +223990,8 @@ var init_control_plane_file_store = __esm({
|
|
|
223844
223990
|
agentCredentials,
|
|
223845
223991
|
principalBridges,
|
|
223846
223992
|
// 交接操作无按公司列举的读面(按 operationId / accountId 定位),直接读 protected map。
|
|
223847
|
-
offboardings: [...this.offboardings.values()]
|
|
223993
|
+
offboardings: [...this.offboardings.values()],
|
|
223994
|
+
roleMigrations: await this.listMemberRoleMigrations()
|
|
223848
223995
|
};
|
|
223849
223996
|
fs25.writeFileSync(this.file, JSON.stringify(snap, null, 2));
|
|
223850
223997
|
}
|
|
@@ -225071,6 +225218,176 @@ var init_org_model_backfill = __esm({
|
|
|
225071
225218
|
}
|
|
225072
225219
|
});
|
|
225073
225220
|
|
|
225221
|
+
// ../server/src/org-identity-migration.ts
|
|
225222
|
+
function e1Candidate(members) {
|
|
225223
|
+
if (members.length === 0) return void 0;
|
|
225224
|
+
if (members.some((m2) => !m2.createdAt)) return void 0;
|
|
225225
|
+
return [...members].sort((a, b2) => {
|
|
225226
|
+
const ta = a.createdAt;
|
|
225227
|
+
const tb = b2.createdAt;
|
|
225228
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
225229
|
+
return a.accountId < b2.accountId ? -1 : a.accountId > b2.accountId ? 1 : 0;
|
|
225230
|
+
})[0];
|
|
225231
|
+
}
|
|
225232
|
+
async function inventoryOrgIdentity(deps, opts = {}) {
|
|
225233
|
+
const listMembers = deps.listMembersWithCreatedAt ?? ((cid) => deps.store.listMembers(cid));
|
|
225234
|
+
const companies = (await deps.store.listCompanies()).filter((c) => opts.companyId === void 0 || c.id === opts.companyId);
|
|
225235
|
+
const rows = [];
|
|
225236
|
+
for (const c of companies) {
|
|
225237
|
+
const members = await listMembers(c.id);
|
|
225238
|
+
rows.push(decide(c, members, opts.manifest));
|
|
225239
|
+
}
|
|
225240
|
+
return {
|
|
225241
|
+
rows,
|
|
225242
|
+
blocked: rows.filter((r) => r.decision.startsWith("blocked")).length,
|
|
225243
|
+
pending: rows.filter((r) => (r.decision === "remap" || r.decision === "manifest") && r.changes.length > 0).length
|
|
225244
|
+
};
|
|
225245
|
+
}
|
|
225246
|
+
function decide(company, members, manifest) {
|
|
225247
|
+
const owners = members.filter((m2) => m2.role === "owner");
|
|
225248
|
+
const base = {
|
|
225249
|
+
companyId: company.id,
|
|
225250
|
+
companyName: company.name,
|
|
225251
|
+
status: company.status,
|
|
225252
|
+
members: members.length,
|
|
225253
|
+
owners: owners.length,
|
|
225254
|
+
admins: members.filter((m2) => m2.role === "admin").length,
|
|
225255
|
+
plainMembers: members.filter((m2) => m2.role === "member").length,
|
|
225256
|
+
rowsWithoutCreatedAt: members.filter((m2) => !m2.createdAt).length,
|
|
225257
|
+
changes: []
|
|
225258
|
+
};
|
|
225259
|
+
if (members.length === 0) {
|
|
225260
|
+
return { ...base, decision: "blocked:empty", reason: "\u7EC4\u7EC7\u6CA1\u6709\u4EFB\u4F55\u6210\u5458\u884C\u2014\u2014\u5220\u8FD8\u662F\u7559\u7531\u4EBA\u51B3\u5B9A\uFF0C\u811A\u672C\u4E0D\u78B0" };
|
|
225261
|
+
}
|
|
225262
|
+
const pinned = manifest?.owners[company.id];
|
|
225263
|
+
if (pinned !== void 0) {
|
|
225264
|
+
if (!members.some((m2) => m2.accountId === pinned)) {
|
|
225265
|
+
return {
|
|
225266
|
+
...base,
|
|
225267
|
+
decision: "blocked:no-owner",
|
|
225268
|
+
reason: `manifest \u6307\u5B9A\u7684 ${pinned} \u4E0D\u662F\u8BE5\u7EC4\u7EC7\u73B0\u5B58\u6210\u5458\u884C\u2014\u2014\u6307\u9519\u4EBA\u6BD4\u4E0D\u6307\u66F4\u7CDF\uFF0C\u62D2\u7EDD\u6267\u884C`
|
|
225269
|
+
};
|
|
225270
|
+
}
|
|
225271
|
+
return {
|
|
225272
|
+
...base,
|
|
225273
|
+
decision: "manifest",
|
|
225274
|
+
targetOwnerAccountId: pinned,
|
|
225275
|
+
changes: plannedChanges(members, pinned),
|
|
225276
|
+
reason: `\u6309\u51BB\u7ED3 manifest\uFF08${manifest.manifestVersion}\uFF09\u6307\u5B9A ${pinned} \u4E3A\u7EC4\u7EC7\u7BA1\u7406\u5458`
|
|
225277
|
+
};
|
|
225278
|
+
}
|
|
225279
|
+
const e1 = e1Candidate(members);
|
|
225280
|
+
if (!e1) {
|
|
225281
|
+
return {
|
|
225282
|
+
...base,
|
|
225283
|
+
decision: "blocked:no-created-at",
|
|
225284
|
+
reason: `\u6709 ${base.rowsWithoutCreatedAt} \u884C\u6210\u5458\u6CA1\u6709 created_at\uFF08\u8BE5\u5217\u662F expand \u671F\u624D\u52A0\u7684\uFF09\uFF0CE1 \u4E0D\u6210\u7ACB \u2192 \u5199\u8FDB manifest \u7531\u4EBA\u6307\u5B9A\u521B\u5EFA\u8005`
|
|
225285
|
+
};
|
|
225286
|
+
}
|
|
225287
|
+
if (e1.role === "member") {
|
|
225288
|
+
return {
|
|
225289
|
+
...base,
|
|
225290
|
+
decision: "blocked:e1-is-plain-member",
|
|
225291
|
+
e1AccountId: e1.accountId,
|
|
225292
|
+
reason: `\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId} \u5F53\u524D\u662F\u666E\u901A\u6210\u5458\uFF08\u53EF\u80FD\u88AB\u964D\u7EA7\u8FC7\uFF0C\u6216\u6570\u636E\u6765\u81EA\u8FC1\u79FB\u6307\u6D3E\uFF09\u2192 \u7531\u4EBA\u6307\u5B9A`
|
|
225293
|
+
};
|
|
225294
|
+
}
|
|
225295
|
+
if (owners.length === 0) {
|
|
225296
|
+
return {
|
|
225297
|
+
...base,
|
|
225298
|
+
decision: "blocked:no-owner",
|
|
225299
|
+
e1AccountId: e1.accountId,
|
|
225300
|
+
reason: "\u4E00\u4E2A owner \u90FD\u6CA1\u6709 \u2192 \u7531\u4EBA\u6307\u5B9A\uFF08\u8C01\u5F53\u552F\u4E00\u6301\u6709\u4EBA\u662F\u4E0D\u53EF\u9006\u7684\u4EBA\u4E8B\u51B3\u5B9A\uFF09"
|
|
225301
|
+
};
|
|
225302
|
+
}
|
|
225303
|
+
const changes = plannedChanges(members, e1.accountId);
|
|
225304
|
+
if (changes.length === 0) {
|
|
225305
|
+
return {
|
|
225306
|
+
...base,
|
|
225307
|
+
decision: "ok",
|
|
225308
|
+
e1AccountId: e1.accountId,
|
|
225309
|
+
targetOwnerAccountId: e1.accountId,
|
|
225310
|
+
reason: "\u5DF2\u7ECF\u6070\u6709\u4E00\u4F4D\u7EC4\u7EC7\u7BA1\u7406\u5458\u3001\u4E14\u5C31\u662F\u6700\u65E9\u52A0\u5165\u7684\u90A3\u4F4D\u2014\u2014\u65E0\u9700\u6539\u52A8"
|
|
225311
|
+
};
|
|
225312
|
+
}
|
|
225313
|
+
return {
|
|
225314
|
+
...base,
|
|
225315
|
+
decision: "remap",
|
|
225316
|
+
e1AccountId: e1.accountId,
|
|
225317
|
+
targetOwnerAccountId: e1.accountId,
|
|
225318
|
+
changes,
|
|
225319
|
+
reason: `E1\uFF1A\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId}\uFF08\u5F53\u524D ${e1.role}\uFF09\u2192 \u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u5176\u4F59\u539F owner \u964D\u4E3A\u7BA1\u7406\u5458`
|
|
225320
|
+
};
|
|
225321
|
+
}
|
|
225322
|
+
function plannedChanges(members, targetOwner) {
|
|
225323
|
+
const out = [];
|
|
225324
|
+
for (const m2 of members) {
|
|
225325
|
+
if (m2.accountId === targetOwner) {
|
|
225326
|
+
if (m2.role !== "owner") out.push({ accountId: m2.accountId, roleBefore: m2.role, roleAfter: "owner" });
|
|
225327
|
+
continue;
|
|
225328
|
+
}
|
|
225329
|
+
if (m2.role === "owner") out.push({ accountId: m2.accountId, roleBefore: "owner", roleAfter: "admin" });
|
|
225330
|
+
}
|
|
225331
|
+
return out;
|
|
225332
|
+
}
|
|
225333
|
+
async function backfillOrgIdentity(deps, opts = {}) {
|
|
225334
|
+
const log3 = opts.log ?? (() => {
|
|
225335
|
+
});
|
|
225336
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
225337
|
+
const inv = await inventoryOrgIdentity(deps, opts);
|
|
225338
|
+
let applied = 0;
|
|
225339
|
+
let auditRows = 0;
|
|
225340
|
+
for (const row of inv.rows) {
|
|
225341
|
+
if (row.decision !== "remap" && row.decision !== "manifest") {
|
|
225342
|
+
if (row.decision.startsWith("blocked")) log3(` \u26D4 ${row.companyId} ${row.decision}\uFF1A${row.reason}`);
|
|
225343
|
+
continue;
|
|
225344
|
+
}
|
|
225345
|
+
if (row.changes.length === 0) continue;
|
|
225346
|
+
const target = row.targetOwnerAccountId;
|
|
225347
|
+
const desc = row.changes.map((c) => `${c.accountId} ${c.roleBefore}\u2192${c.roleAfter}`).join("\uFF0C");
|
|
225348
|
+
log3(` ${row.companyId}\uFF1A${desc}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
|
|
225349
|
+
if (!opts.apply) continue;
|
|
225350
|
+
const evidence = row.decision === "manifest" ? "manifest" : "E1";
|
|
225351
|
+
const res = await deps.store.migrateSoleCompanyOwner(row.companyId, target, {
|
|
225352
|
+
evidence,
|
|
225353
|
+
...opts.manifest?.manifestVersion !== void 0 ? { manifestVersion: opts.manifest.manifestVersion } : {},
|
|
225354
|
+
appliedAt: now()
|
|
225355
|
+
});
|
|
225356
|
+
if (!res.ok) {
|
|
225357
|
+
throw new Error(`${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08\u90A3\u4E00\u884C\u4E0D\u5728\u8BE5\u7EC4\u7EC7\u7684\u6210\u5458\u8868\u91CC\uFF09`);
|
|
225358
|
+
}
|
|
225359
|
+
applied += 1;
|
|
225360
|
+
auditRows += res.auditRows.length;
|
|
225361
|
+
}
|
|
225362
|
+
return { ...inv, applied, auditRows };
|
|
225363
|
+
}
|
|
225364
|
+
async function rollbackOrgIdentity(deps, opts) {
|
|
225365
|
+
const log3 = opts.log ?? (() => {
|
|
225366
|
+
});
|
|
225367
|
+
const audit2 = await deps.store.listMemberRoleMigrations(opts.companyId);
|
|
225368
|
+
const plan = [];
|
|
225369
|
+
let reverted = 0;
|
|
225370
|
+
for (const r of [...audit2].reverse()) {
|
|
225371
|
+
const member = await deps.store.getMember(r.companyId, r.accountId);
|
|
225372
|
+
if (!member) {
|
|
225373
|
+
log3(` \u8DF3\u8FC7 ${r.accountId}\uFF1A\u6210\u5458\u884C\u5DF2\u4E0D\u5B58\u5728\uFF08\u88AB\u79FB\u9664\u8FC7\uFF09`);
|
|
225374
|
+
continue;
|
|
225375
|
+
}
|
|
225376
|
+
if (member.role === r.roleBefore) continue;
|
|
225377
|
+
plan.push({ companyId: r.companyId, accountId: r.accountId, from: member.role, to: r.roleBefore });
|
|
225378
|
+
log3(` ${r.companyId} ${r.accountId} ${member.role}\u2192${r.roleBefore}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
|
|
225379
|
+
if (!opts.apply) continue;
|
|
225380
|
+
await deps.store.setMemberRoleOnly(r.companyId, r.accountId, r.roleBefore);
|
|
225381
|
+
reverted += 1;
|
|
225382
|
+
}
|
|
225383
|
+
return { plan, reverted };
|
|
225384
|
+
}
|
|
225385
|
+
var init_org_identity_migration = __esm({
|
|
225386
|
+
"../server/src/org-identity-migration.ts"() {
|
|
225387
|
+
"use strict";
|
|
225388
|
+
}
|
|
225389
|
+
});
|
|
225390
|
+
|
|
225074
225391
|
// ../server/src/config-audit.ts
|
|
225075
225392
|
function shallowDiff(before, after) {
|
|
225076
225393
|
const out = {};
|
|
@@ -232382,6 +232699,68 @@ var init_runtime_tenancy = __esm({
|
|
|
232382
232699
|
}
|
|
232383
232700
|
});
|
|
232384
232701
|
|
|
232702
|
+
// ../server/src/governance/agent-manage-guard.ts
|
|
232703
|
+
function isCompanyManager(role) {
|
|
232704
|
+
return role === "owner" || role === "admin";
|
|
232705
|
+
}
|
|
232706
|
+
function isAssistantAgent(subject) {
|
|
232707
|
+
return subject.positions.includes(ASSISTANT_POSITION);
|
|
232708
|
+
}
|
|
232709
|
+
function mayManageAgent(caller, subject) {
|
|
232710
|
+
if (!isAssistantAgent(subject)) return isCompanyManager(caller.role);
|
|
232711
|
+
if (subject.boundHumanActorId === void 0) return isCompanyManager(caller.role);
|
|
232712
|
+
if (subject.boundHumanActorId === caller.actorId) return true;
|
|
232713
|
+
if (subject.boundHumanReportsTo !== void 0 && subject.boundHumanReportsTo === caller.actorId) return true;
|
|
232714
|
+
return isCompanyManager(caller.role);
|
|
232715
|
+
}
|
|
232716
|
+
async function assertMayManageAgent(guard, ctx, agent) {
|
|
232717
|
+
if (!guard) return;
|
|
232718
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232719
|
+
const subject = { positions: agent.positions };
|
|
232720
|
+
if (isAssistantAgent(subject)) {
|
|
232721
|
+
const human = await guard.boundHuman(ctx.companyId, agent.id);
|
|
232722
|
+
if (human) {
|
|
232723
|
+
subject.boundHumanActorId = human;
|
|
232724
|
+
subject.boundHumanReportsTo = await guard.reportsTo(ctx.companyId, human) ?? void 0;
|
|
232725
|
+
}
|
|
232726
|
+
}
|
|
232727
|
+
if (mayManageAgent({ role, actorId: ctx.callerActorId }, subject)) return;
|
|
232728
|
+
throw new AgentManageForbiddenError(
|
|
232729
|
+
isAssistantAgent(subject) ? "\u53EA\u6709\u672C\u4EBA\u3001\u5176\u76F4\u63A5\u4E0A\u7EA7\u6216\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u8FD9\u4E2A\u52A9\u7406" : "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u4E13\u5BB6 Agent"
|
|
232730
|
+
);
|
|
232731
|
+
}
|
|
232732
|
+
async function assertCompanyManager(guard, ctx, what) {
|
|
232733
|
+
if (!guard) return;
|
|
232734
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232735
|
+
if (isCompanyManager(role)) return;
|
|
232736
|
+
throw new AgentManageForbiddenError(`\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5${what}`);
|
|
232737
|
+
}
|
|
232738
|
+
async function assertMayCreateAgent(guard, ctx) {
|
|
232739
|
+
return assertCompanyManager(guard, ctx, "\u65B0\u5EFA Agent");
|
|
232740
|
+
}
|
|
232741
|
+
async function assertMaySelfOrManage(guard, ctx, targetActorId) {
|
|
232742
|
+
if (!guard) return;
|
|
232743
|
+
if (ctx.callerActorId === targetActorId) return;
|
|
232744
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232745
|
+
if (isCompanyManager(role)) return;
|
|
232746
|
+
throw new AgentManageForbiddenError("\u53EA\u80FD\u4FEE\u6539\u81EA\u5DF1\u7684\u6863\u6848\uFF1B\u6539\u4ED6\u4EBA\u7684\u9700\u8981\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u6743\u9650");
|
|
232747
|
+
}
|
|
232748
|
+
var ASSISTANT_POSITION, AgentManageForbiddenError;
|
|
232749
|
+
var init_agent_manage_guard = __esm({
|
|
232750
|
+
"../server/src/governance/agent-manage-guard.ts"() {
|
|
232751
|
+
"use strict";
|
|
232752
|
+
ASSISTANT_POSITION = "assistant";
|
|
232753
|
+
AgentManageForbiddenError = class extends Error {
|
|
232754
|
+
status = 403;
|
|
232755
|
+
code = "FORBIDDEN";
|
|
232756
|
+
constructor(message) {
|
|
232757
|
+
super(message);
|
|
232758
|
+
this.name = "AgentManageForbiddenError";
|
|
232759
|
+
}
|
|
232760
|
+
};
|
|
232761
|
+
}
|
|
232762
|
+
});
|
|
232763
|
+
|
|
232385
232764
|
// ../server/src/domains/actors/routes.ts
|
|
232386
232765
|
async function asApiError(run) {
|
|
232387
232766
|
try {
|
|
@@ -232470,6 +232849,33 @@ function mapMarketError(err) {
|
|
|
232470
232849
|
}
|
|
232471
232850
|
function actorsDomain(opts) {
|
|
232472
232851
|
const { resolveCtx } = opts;
|
|
232852
|
+
const asForbidden = async (run) => {
|
|
232853
|
+
try {
|
|
232854
|
+
await run();
|
|
232855
|
+
} catch (e) {
|
|
232856
|
+
if (e instanceof AgentManageForbiddenError) throw new ApiError(e.status, e.code, e.message);
|
|
232857
|
+
throw e;
|
|
232858
|
+
}
|
|
232859
|
+
};
|
|
232860
|
+
const guardAgentWrite = async (req, actor) => {
|
|
232861
|
+
const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
|
|
232862
|
+
if (actor.kind !== "agent") {
|
|
232863
|
+
await asForbidden(() => assertMaySelfOrManage(opts.agentManageGuard, ctx, actor.id));
|
|
232864
|
+
return;
|
|
232865
|
+
}
|
|
232866
|
+
await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
|
|
232867
|
+
};
|
|
232868
|
+
const guardPositionsChange = async (req, existing, nextPositions) => {
|
|
232869
|
+
if (existing.kind !== "agent" || nextPositions === void 0) return;
|
|
232870
|
+
if (isAssistantAgent({ positions: existing.roles }) === isAssistantAgent({ positions: nextPositions })) return;
|
|
232871
|
+
const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
|
|
232872
|
+
await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: existing.id, positions: nextPositions }));
|
|
232873
|
+
};
|
|
232874
|
+
const guardExistingActorWrite = async (req, service, actorId) => {
|
|
232875
|
+
const existing = await service.getActor(actorId);
|
|
232876
|
+
if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
|
|
232877
|
+
await guardAgentWrite(req, existing);
|
|
232878
|
+
};
|
|
232473
232879
|
const revealLimits = opts.credentialRevealLimits ?? { perSession: 200, perSessionPerKey: 20 };
|
|
232474
232880
|
const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
|
|
232475
232881
|
const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
|
|
@@ -232522,6 +232928,10 @@ function actorsDomain(opts) {
|
|
|
232522
232928
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232523
232929
|
const b2 = req.body;
|
|
232524
232930
|
if (!b2?.id || !b2.kind || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / kind / name");
|
|
232931
|
+
await asForbidden(() => assertMayCreateAgent(opts.agentManageGuard, {
|
|
232932
|
+
companyId: req.auth.companyId,
|
|
232933
|
+
callerActorId: req.auth.actor
|
|
232934
|
+
}));
|
|
232525
232935
|
const { id, kind, name } = b2;
|
|
232526
232936
|
const positions = positionsFromPayload(b2);
|
|
232527
232937
|
const avatar = avatarRefFromPayload(b2);
|
|
@@ -232548,10 +232958,12 @@ function actorsDomain(opts) {
|
|
|
232548
232958
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232549
232959
|
const existing = await service.getActor(req.params.id);
|
|
232550
232960
|
if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${req.params.id}`);
|
|
232961
|
+
await guardAgentWrite(req, existing);
|
|
232551
232962
|
const b2 = req.body ?? {};
|
|
232552
232963
|
const positions = positionsFromPayload(b2);
|
|
232553
232964
|
const avatar = avatarRefFromPayload(b2);
|
|
232554
232965
|
const tags = tagsFromPayload(b2);
|
|
232966
|
+
await guardPositionsChange(req, existing, positions);
|
|
232555
232967
|
const {
|
|
232556
232968
|
positions: _positions,
|
|
232557
232969
|
id: _id,
|
|
@@ -232582,23 +232994,27 @@ function actorsDomain(opts) {
|
|
|
232582
232994
|
});
|
|
232583
232995
|
router.delete("/api/actors/:id", async (req) => {
|
|
232584
232996
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232997
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232585
232998
|
await service.disableActor(req.params.id, req.auth.actor);
|
|
232586
232999
|
return { status: 200, body: { ok: true } };
|
|
232587
233000
|
});
|
|
232588
233001
|
router.post("/api/actors/:id/avatar", async (req) => {
|
|
232589
233002
|
const { service, blobs } = await resolveCtx(req.auth.companyId);
|
|
232590
233003
|
if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
|
|
233004
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232591
233005
|
const ref2 = await storeImageBlob(blobs, req.body?.data);
|
|
232592
233006
|
const actor = await service.setActorAvatar(req.params.id, ref2, req.auth.actor);
|
|
232593
233007
|
return { status: 201, body: { avatar: actor.avatar } };
|
|
232594
233008
|
});
|
|
232595
233009
|
router.delete("/api/actors/:id/avatar", async (req) => {
|
|
232596
233010
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233011
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232597
233012
|
await service.setActorAvatar(req.params.id, null, req.auth.actor);
|
|
232598
233013
|
return { status: 200, body: { ok: true } };
|
|
232599
233014
|
});
|
|
232600
233015
|
router.post("/api/teams/:id/icon", async (req) => {
|
|
232601
233016
|
const { service, blobs } = await resolveCtx(req.auth.companyId);
|
|
233017
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
|
|
232602
233018
|
if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
|
|
232603
233019
|
const ref2 = await storeImageBlob(blobs, req.body?.data);
|
|
232604
233020
|
await service.setTeamIcon(req.params.id, ref2);
|
|
@@ -232606,6 +233022,7 @@ function actorsDomain(opts) {
|
|
|
232606
233022
|
});
|
|
232607
233023
|
router.delete("/api/teams/:id/icon", async (req) => {
|
|
232608
233024
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233025
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
|
|
232609
233026
|
await service.setTeamIcon(req.params.id, null);
|
|
232610
233027
|
return { status: 200, body: { ok: true } };
|
|
232611
233028
|
});
|
|
@@ -232617,6 +233034,7 @@ function actorsDomain(opts) {
|
|
|
232617
233034
|
});
|
|
232618
233035
|
router.post("/api/actors/:id/config", async (req) => {
|
|
232619
233036
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233037
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232620
233038
|
const b2 = req.body ?? {};
|
|
232621
233039
|
const checkString = (key2) => {
|
|
232622
233040
|
if (b2[key2] !== void 0 && typeof b2[key2] !== "string") {
|
|
@@ -232678,6 +233096,7 @@ function actorsDomain(opts) {
|
|
|
232678
233096
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232679
233097
|
const actorId = req.params.id;
|
|
232680
233098
|
if (!await service.getActor(actorId)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
|
|
233099
|
+
await guardExistingActorWrite(req, service, actorId);
|
|
232681
233100
|
const raw = req.body?.candidates;
|
|
232682
233101
|
if (!Array.isArray(raw)) throw new ApiError(400, "BAD_REQUEST", "candidates \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
232683
233102
|
if (raw.length > 20) throw new ApiError(400, "BAD_REQUEST", "\u5019\u9009\u8FD0\u884C\u65F6\u6700\u591A 20 \u6761");
|
|
@@ -232712,6 +233131,7 @@ function actorsDomain(opts) {
|
|
|
232712
233131
|
});
|
|
232713
233132
|
router.post("/api/actors/:id/config/rollback", async (req) => {
|
|
232714
233133
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233134
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232715
233135
|
const b2 = req.body;
|
|
232716
233136
|
if (typeof b2?.version !== "number") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 version");
|
|
232717
233137
|
const cfg = await service.rollbackConfig(req.params.id, b2.version, req.auth.actor);
|
|
@@ -232723,6 +233143,7 @@ function actorsDomain(opts) {
|
|
|
232723
233143
|
});
|
|
232724
233144
|
router.patch("/api/actors/:id/settings", async (req) => {
|
|
232725
233145
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233146
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232726
233147
|
const b2 = req.body ?? {};
|
|
232727
233148
|
if (b2.memoryEnabled !== void 0 && typeof b2.memoryEnabled !== "boolean") {
|
|
232728
233149
|
throw new ApiError(400, "BAD_REQUEST", "memoryEnabled \u5FC5\u987B\u662F boolean");
|
|
@@ -232742,6 +233163,7 @@ function actorsDomain(opts) {
|
|
|
232742
233163
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232743
233164
|
const b2 = req.body;
|
|
232744
233165
|
if (!b2?.id || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / name");
|
|
233166
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u65B0\u5EFA\u56E2\u961F"));
|
|
232745
233167
|
await service.upsertTeam({
|
|
232746
233168
|
id: b2.id,
|
|
232747
233169
|
name: b2.name,
|
|
@@ -232762,6 +233184,7 @@ function actorsDomain(opts) {
|
|
|
232762
233184
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232763
233185
|
const b2 = req.body;
|
|
232764
233186
|
if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
|
|
233187
|
+
await guardExistingActorWrite(req, service, b2.actorId);
|
|
232765
233188
|
const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
|
|
232766
233189
|
const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
|
|
232767
233190
|
if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
|
|
@@ -232770,6 +233193,7 @@ function actorsDomain(opts) {
|
|
|
232770
233193
|
});
|
|
232771
233194
|
router.delete("/api/bindings/:actorId", async (req) => {
|
|
232772
233195
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233196
|
+
await guardExistingActorWrite(req, service, req.params.actorId);
|
|
232773
233197
|
await service.unbind(req.params.actorId, req.auth.actor);
|
|
232774
233198
|
return { status: 200, body: { ok: true } };
|
|
232775
233199
|
});
|
|
@@ -232805,6 +233229,7 @@ function actorsDomain(opts) {
|
|
|
232805
233229
|
});
|
|
232806
233230
|
router.put("/api/actors/:id/connectors/:connectorId", async (req) => {
|
|
232807
233231
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233232
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232808
233233
|
const b2 = req.body;
|
|
232809
233234
|
if (typeof b2?.enabled !== "boolean") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 enabled (boolean)");
|
|
232810
233235
|
const conn = b2.enabled ? await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, true, req.auth.actor) : await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, false);
|
|
@@ -233973,6 +234398,7 @@ var init_routes3 = __esm({
|
|
|
233973
234398
|
init_skill_materializer();
|
|
233974
234399
|
init_connector_skills();
|
|
233975
234400
|
init_runtime_tenancy();
|
|
234401
|
+
init_agent_manage_guard();
|
|
233976
234402
|
CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
|
|
233977
234403
|
RevealRateLimiter = class {
|
|
233978
234404
|
constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
|
|
@@ -234304,7 +234730,7 @@ function createActorsDomain(opts) {
|
|
|
234304
234730
|
return {
|
|
234305
234731
|
service: defaultCtx.service,
|
|
234306
234732
|
resolveCtx,
|
|
234307
|
-
register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
|
|
234733
|
+
register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {}, ...opts.agentManageGuard ? { agentManageGuard: opts.agentManageGuard } : {} })
|
|
234308
234734
|
};
|
|
234309
234735
|
}
|
|
234310
234736
|
var import_node_crypto55;
|
|
@@ -241577,11 +242003,34 @@ var init_service6 = __esm({
|
|
|
241577
242003
|
async isEmployedElsewhere(companyId, accountId) {
|
|
241578
242004
|
return isEmployedElsewhere(this.store, companyId, accountId);
|
|
241579
242005
|
}
|
|
241580
|
-
/**
|
|
242006
|
+
/**
|
|
242007
|
+
* 当前用户所属的公司(按 membership 聚合)。**只返回 `status='active'` 的**。
|
|
242008
|
+
*
|
|
242009
|
+
* 🚨 过滤 status 是「解散组织」能真正生效的一半(组织三层身份 ADR D9.2):解散取的是可审计的
|
|
242010
|
+
* 停用语义(`status='suspended'` + `dissolved_at/by`),成员行**保留**。不在这里过滤的话,
|
|
242011
|
+
* 被解散的组织仍然会出现在顶栏切换器上、`CompanyGate` 也仍把这个人算作「有组织」——
|
|
242012
|
+
* 解散就只是换了个状态字,人照进不误。口径与 `resolveCurrentCompany` 的无 selector 分支对齐
|
|
242013
|
+
* (那一支本来就只挑 active)。某人全部组织都被解散后,按既有逻辑落到建团队页。
|
|
242014
|
+
*/
|
|
241581
242015
|
async listForUser(accountId) {
|
|
241582
242016
|
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
241583
242017
|
const fetched = await Promise.all(memberships.map((m2) => this.store.getCompany(m2.companyId)));
|
|
241584
|
-
return fetched.filter((c) => c !== null && c !== void 0);
|
|
242018
|
+
return fetched.filter((c) => c !== null && c !== void 0 && c.status === "active");
|
|
242019
|
+
}
|
|
242020
|
+
/**
|
|
242021
|
+
* 「我在哪些组织里还是组织管理员」——注销账号的跨组织检查(ADR D9.3)。
|
|
242022
|
+
* 只看 `status='active'` 的组织:已解散的那家里是不是 owner 已经无关紧要。
|
|
242023
|
+
* **不能只看当前组织**:注销是账号级动作,漏一家就等于把那家组织甩成无人可管。
|
|
242024
|
+
*/
|
|
242025
|
+
async listOwnedActiveCompanies(accountId) {
|
|
242026
|
+
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
242027
|
+
const out = [];
|
|
242028
|
+
for (const m2 of memberships) {
|
|
242029
|
+
if (m2.role !== "owner") continue;
|
|
242030
|
+
const c = await this.store.getCompany(m2.companyId);
|
|
242031
|
+
if (c?.status === "active") out.push(c);
|
|
242032
|
+
}
|
|
242033
|
+
return out;
|
|
241585
242034
|
}
|
|
241586
242035
|
async get(id) {
|
|
241587
242036
|
return this.store.getCompany(id);
|
|
@@ -241646,11 +242095,49 @@ var init_service6 = __esm({
|
|
|
241646
242095
|
return account;
|
|
241647
242096
|
}
|
|
241648
242097
|
/**
|
|
241649
|
-
*
|
|
241650
|
-
*
|
|
242098
|
+
* 授予某邮箱在某公司的组织管理员(ADR 0009 切片5:bootstrap 种子 + `grant-owner` 兜底)。
|
|
242099
|
+
*
|
|
242100
|
+
* 🚨 **它必须自己守 I1**(组织三层身份 ADR D2.4)。老实现是**无条件** upsert `role='owner'`,
|
|
242101
|
+
* 而调用点之一是 serve 的 bootstrap(`OASIS_BOOTSTRAP_OWNER`,在**启动路径**上)——该组织若已有
|
|
242102
|
+
* 组织管理员,它会写出第二位,在部分唯一索引 `company_members_one_owner_idx` 下直接抛错,
|
|
242103
|
+
* **等于 serve 起不来**。所以三分支:
|
|
242104
|
+
* - 目标已是该组织 owner → no-op,返回既有那行;
|
|
242105
|
+
* - 组织已有**别的** owner → **不写 owner**、`console.warn`,返回 `{ granted: false, existingOwner }`,
|
|
242106
|
+
* CLI 侧据此非零退出。但**仍然保证他是成员**:还不是成员就落一条 `admin`(已是成员的角色一个字不动)。
|
|
242107
|
+
* 为什么不是干脆「什么都不写」:这条通道的本意是 ADR 0009 的「第一个 owner 不会把自己锁在门外」
|
|
242108
|
+
* ——`OASIS_BOOTSTRAP_OWNER_EMAIL` 要让创始人**能登录并进得去管理面**。什么都不写的话,
|
|
242109
|
+
* 首装时若默认公司迁移已经指派过一位 owner,创始人就只有账号、没有任何 membership,
|
|
242110
|
+
* 登录后被 `CompanyGate` 扔到建团队页——兜底通道当场失效。管理面的闸是 `requireOwnerOrAdmin`,
|
|
242111
|
+
* admin 一样进得去;而「谁是唯一持有人」不该被一次装机悄悄换人。
|
|
242112
|
+
* - 组织无 owner(首次 bootstrap、或存量 0-owner 的脏数据)→ 照旧指派为 owner。
|
|
242113
|
+
* 「换人」一律走转交(`POST /api/companies/:id/owner`),不从这条兜底通道走。
|
|
241651
242114
|
*/
|
|
241652
242115
|
async grantOwner(companyId, email2, name) {
|
|
241653
242116
|
const account = await this.ensureAccount(email2, name);
|
|
242117
|
+
const members = await this.store.listMembers(companyId);
|
|
242118
|
+
const currentOwner = members.find((m2) => m2.role === "owner");
|
|
242119
|
+
if (currentOwner && currentOwner.accountId !== account.id) {
|
|
242120
|
+
const existingSelf = members.find((m2) => m2.accountId === account.id);
|
|
242121
|
+
if (existingSelf) {
|
|
242122
|
+
console.warn(
|
|
242123
|
+
`[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF08\u4ED6\u5DF2\u662F\u672C\u7EC4\u7EC7 ${existingSelf.role}\uFF0C\u89D2\u8272\u672A\u6539\uFF09\uFF1B\u5982\u9700\u66F4\u6362\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
|
|
242124
|
+
);
|
|
242125
|
+
return { granted: false, membership: existingSelf, existingOwner: currentOwner };
|
|
242126
|
+
}
|
|
242127
|
+
const asAdmin = {
|
|
242128
|
+
companyId,
|
|
242129
|
+
accountId: account.id,
|
|
242130
|
+
actorId: account.id,
|
|
242131
|
+
role: "admin",
|
|
242132
|
+
onboardingCompletedAt: this.now()
|
|
242133
|
+
};
|
|
242134
|
+
await this.store.upsertMember(asAdmin);
|
|
242135
|
+
console.warn(
|
|
242136
|
+
`[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF1B\u5DF2\u6539\u4E3A\u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002\u5982\u9700\u66F4\u6362\u6301\u6709\u4EBA\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
|
|
242137
|
+
);
|
|
242138
|
+
return { granted: false, membership: asAdmin, existingOwner: currentOwner };
|
|
242139
|
+
}
|
|
242140
|
+
if (currentOwner) return { granted: false, membership: currentOwner };
|
|
241654
242141
|
const membership = {
|
|
241655
242142
|
companyId,
|
|
241656
242143
|
accountId: account.id,
|
|
@@ -241659,7 +242146,7 @@ var init_service6 = __esm({
|
|
|
241659
242146
|
onboardingCompletedAt: this.now()
|
|
241660
242147
|
};
|
|
241661
242148
|
await this.store.upsertMember(membership);
|
|
241662
|
-
return membership;
|
|
242149
|
+
return { granted: true, membership };
|
|
241663
242150
|
}
|
|
241664
242151
|
/**
|
|
241665
242152
|
* 当前公司解析(动态、随登录用户走):
|
|
@@ -241672,6 +242159,7 @@ var init_service6 = __esm({
|
|
|
241672
242159
|
let company;
|
|
241673
242160
|
if (selector) {
|
|
241674
242161
|
company = await this.store.getCompany(selector) ?? await this.store.getCompanyBySlug(selector);
|
|
242162
|
+
if (company && company.status !== "active") company = null;
|
|
241675
242163
|
} else {
|
|
241676
242164
|
company = null;
|
|
241677
242165
|
for (const m2 of await this.store.listMembershipsForAccount(accountId)) {
|
|
@@ -241774,11 +242262,91 @@ var init_service6 = __esm({
|
|
|
241774
242262
|
*/
|
|
241775
242263
|
async changeMemberRole(companyId, accountId, role) {
|
|
241776
242264
|
if (!ROLES.includes(role)) throw new CompanyError(400, "BAD_ROLE", `role \u987B\u4E3A ${ROLES.join(" | ")}`);
|
|
242265
|
+
if (role === "owner") {
|
|
242266
|
+
throw new CompanyError(400, "OWNER_NOT_ASSIGNABLE", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EA\u80FD\u7ECF\u8F6C\u4EA4\u4EA7\u751F\uFF0C\u4E0D\u80FD\u76F4\u63A5\u6307\u6D3E");
|
|
242267
|
+
}
|
|
241777
242268
|
await this.requireCompany(companyId);
|
|
242269
|
+
const target = await this.store.getMember(companyId, accountId);
|
|
242270
|
+
if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242271
|
+
if (target.role === "owner") throw this.ownerProtected();
|
|
241778
242272
|
const res = await this.store.updateMemberRoleIfNotLastOwner(companyId, accountId, role);
|
|
241779
242273
|
if (res.ok) return res.member;
|
|
241780
242274
|
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241781
|
-
throw
|
|
242275
|
+
throw this.ownerProtected();
|
|
242276
|
+
}
|
|
242277
|
+
/** 「组织管理员受保护」的唯一错误来源——成员管理面的四个入口共用,码与文案不各写一份。 */
|
|
242278
|
+
ownerProtected() {
|
|
242279
|
+
return new CompanyError(403, "OWNER_PROTECTED", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u7F16\u8F91\u3001\u964D\u7EA7\u6216\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7");
|
|
242280
|
+
}
|
|
242281
|
+
/**
|
|
242282
|
+
* 转交组织管理员(`POST /api/companies/:id/owner`,ADR D4)。
|
|
242283
|
+
*
|
|
242284
|
+
* 调用者身份从会话解析、**不接受 body 里指定 from**。单事务、一把锁、**先降后升**,
|
|
242285
|
+
* 全部由 `store.setSoleCompanyOwner` 承担(并发语义见它的契约注释)。
|
|
242286
|
+
*
|
|
242287
|
+
* **刻意不做「已达成即 200」的幂等吞掉**:同一转交被重复提交时第二次同样得 409 `NOT_OWNER`。
|
|
242288
|
+
* 吞掉的话「我不是 owner 了」和「我刚转交成功」在响应里长得一样,前端分不出该显示
|
|
242289
|
+
* 「转交成功」还是「你的身份已变更」。
|
|
242290
|
+
*
|
|
242291
|
+
* **不 bump `tokenVersion`**:角色每请求现查(D7),转交不需要作废会话;bump 会把人从
|
|
242292
|
+
* **其它组织**一起踢下线(G10)。
|
|
242293
|
+
*/
|
|
242294
|
+
async transferOwner(companyId, fromAccountId, toAccountId) {
|
|
242295
|
+
if (!toAccountId) throw new CompanyError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId");
|
|
242296
|
+
if (fromAccountId === toAccountId) {
|
|
242297
|
+
throw new CompanyError(400, "SELF_TRANSFER", "\u4E0D\u80FD\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u81EA\u5DF1");
|
|
242298
|
+
}
|
|
242299
|
+
await this.requireActiveCompany(companyId);
|
|
242300
|
+
const res = await this.store.setSoleCompanyOwner(companyId, toAccountId, { requireCurrentOwner: fromAccountId });
|
|
242301
|
+
if (!res.ok) {
|
|
242302
|
+
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
|
|
242303
|
+
if (res.reason === "target_not_listed") {
|
|
242304
|
+
throw new CompanyError(409, "TARGET_NOT_LISTED", "\u5BF9\u65B9\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF08\u65B0\u624B\u6307\u5F15\u672A\u8D70\u5B8C\uFF09\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
|
|
242305
|
+
}
|
|
242306
|
+
if (res.reason === "target_inactive") {
|
|
242307
|
+
throw new CompanyError(409, "TARGET_INACTIVE", "\u5BF9\u65B9\u8D26\u53F7\u5DF2\u505C\u7528\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
|
|
242308
|
+
}
|
|
242309
|
+
throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
|
|
242310
|
+
}
|
|
242311
|
+
const from = res.demoted.find((m2) => m2.accountId === fromAccountId);
|
|
242312
|
+
if (!from) throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
|
|
242313
|
+
return { from, to: res.owner };
|
|
242314
|
+
}
|
|
242315
|
+
/**
|
|
242316
|
+
* 存量映射用的「把这家组织收敛成唯一组织管理员」(ADR D3 回填面)。
|
|
242317
|
+
* 与转交**同一把锁、同一顺序**(先降后升)——两份实现必然漂,而漂出来的差异正好落在
|
|
242318
|
+
* 「中途有没有出现两位持有人」这种只有并发下才现形的地方。
|
|
242319
|
+
*/
|
|
242320
|
+
async setSoleOwner(companyId, toAccountId) {
|
|
242321
|
+
const res = await this.store.setSoleCompanyOwner(companyId, toAccountId);
|
|
242322
|
+
if (!res.ok) {
|
|
242323
|
+
if (res.reason === "target_not_listed") {
|
|
242324
|
+
throw new CompanyError(409, "TARGET_NOT_LISTED", `\u76EE\u6807\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF1A${toAccountId}`);
|
|
242325
|
+
}
|
|
242326
|
+
if (res.reason === "target_inactive") {
|
|
242327
|
+
throw new CompanyError(409, "TARGET_INACTIVE", `\u76EE\u6807\u8D26\u53F7\u5DF2\u505C\u7528\uFF1A${toAccountId}`);
|
|
242328
|
+
}
|
|
242329
|
+
throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
|
|
242330
|
+
}
|
|
242331
|
+
return { owner: res.owner, demoted: res.demoted };
|
|
242332
|
+
}
|
|
242333
|
+
/**
|
|
242334
|
+
* 解散组织(`DELETE /api/companies/:id`,ADR D9.2)——**可审计的停用,不是物理销毁**。
|
|
242335
|
+
* 仅当前组织管理员;「我还是不是 owner」在**同一把锁内**复核(D7.2),不是入口查一次就写。
|
|
242336
|
+
* 断访问靠 `resolveCurrentCompany` 只认 active(D8.4)+ `listForUser` 只列 active(D9.2)。
|
|
242337
|
+
*/
|
|
242338
|
+
async dissolveCompany(companyId, callerAccountId) {
|
|
242339
|
+
await this.requireActiveCompany(companyId);
|
|
242340
|
+
const res = await this.store.dissolveCompanyIfOwner(companyId, callerAccountId, this.now());
|
|
242341
|
+
if (res.ok) return res.company;
|
|
242342
|
+
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
|
|
242343
|
+
throw new CompanyError(403, "FORBIDDEN", "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EF\u4EE5\u89E3\u6563\u7EC4\u7EC7");
|
|
242344
|
+
}
|
|
242345
|
+
/** 公司必须存在**且 active**:已解散 / 已停用的组织上不接受任何写。 */
|
|
242346
|
+
async requireActiveCompany(companyId) {
|
|
242347
|
+
const c = await this.requireCompany(companyId);
|
|
242348
|
+
if (c.status !== "active") throw new CompanyError(404, "COMPANY_NOT_FOUND", `\u516C\u53F8\u4E0D\u53EF\u7528\uFF1A${companyId}`);
|
|
242349
|
+
return c;
|
|
241782
242350
|
}
|
|
241783
242351
|
/**
|
|
241784
242352
|
* 改成员在本公司的昵称(组织模型 ADR §4.1 `company_member.display_name`)。
|
|
@@ -241805,9 +242373,7 @@ var init_service6 = __esm({
|
|
|
241805
242373
|
await this.requireCompany(companyId);
|
|
241806
242374
|
const member = await this.store.getMember(companyId, accountId);
|
|
241807
242375
|
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241808
|
-
if (member.role === "owner"
|
|
241809
|
-
throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
|
|
241810
|
-
}
|
|
242376
|
+
if (member.role === "owner") throw this.ownerProtected();
|
|
241811
242377
|
return member;
|
|
241812
242378
|
}
|
|
241813
242379
|
/**
|
|
@@ -241825,10 +242391,82 @@ var init_service6 = __esm({
|
|
|
241825
242391
|
*/
|
|
241826
242392
|
async removeMember(companyId, accountId) {
|
|
241827
242393
|
await this.requireCompany(companyId);
|
|
241828
|
-
const
|
|
241829
|
-
if (
|
|
242394
|
+
const target = await this.store.getMember(companyId, accountId);
|
|
242395
|
+
if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242396
|
+
if (target.role === "owner") throw this.ownerProtected();
|
|
242397
|
+
const res = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now());
|
|
242398
|
+
if (res.ok) return { member: res.member, releasedAgents: res.releasedAgents };
|
|
241830
242399
|
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241831
|
-
throw
|
|
242400
|
+
throw this.ownerProtected();
|
|
242401
|
+
}
|
|
242402
|
+
/**
|
|
242403
|
+
* 退出组织(`DELETE /api/companies/:id/members/me`,ADR D9.1)——**本人限定**。
|
|
242404
|
+
*
|
|
242405
|
+
* 组织管理员退不出:先转交、或(只剩他一个人时)解散。响应体带 `canDissolve`,
|
|
242406
|
+
* 前端据它把人引到「解散组织」而不是「先转交给谁」(brief:单人团队路径)。
|
|
242407
|
+
* 真正的移除副作用由 `MemberOffboardingService.applyMemberRemoval` 承担——与管理员移除他人
|
|
242408
|
+
* **同一套**,不在这里另写一份。
|
|
242409
|
+
*/
|
|
242410
|
+
async peekSelfLeave(companyId, accountId) {
|
|
242411
|
+
await this.requireActiveCompany(companyId);
|
|
242412
|
+
const me = await this.store.getMember(companyId, accountId);
|
|
242413
|
+
if (!me) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242414
|
+
if (me.role === "owner") {
|
|
242415
|
+
const canDissolve = (await this.store.listMembers(companyId)).length <= 1;
|
|
242416
|
+
throw new CompanyError(
|
|
242417
|
+
409,
|
|
242418
|
+
"OWNER_MUST_TRANSFER",
|
|
242419
|
+
canDissolve ? "\u4F60\u662F\u8FD9\u4E2A\u7EC4\u7EC7\u552F\u4E00\u7684\u6210\u5458\uFF0C\u9000\u51FA\u8BF7\u6539\u4E3A\u89E3\u6563\u7EC4\u7EC7" : "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u80FD\u9000\u51FA\u7EC4\u7EC7\uFF0C\u8BF7\u5148\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u4ED6\u4EBA",
|
|
242420
|
+
{ canDissolve }
|
|
242421
|
+
);
|
|
242422
|
+
}
|
|
242423
|
+
return me;
|
|
242424
|
+
}
|
|
242425
|
+
/**
|
|
242426
|
+
* 注销账号的**跨组织前置检查**(`DELETE /api/accounts/me`,ADR D9.3)。
|
|
242427
|
+
*
|
|
242428
|
+
* 遍历该账号**全部** membership,取其中 `status='active'` 的组织:只要**任意一家**里还是
|
|
242429
|
+
* 组织管理员就拒(409 `OWNER_IN_ORGS`,响应体列出这些组织的 `{id, name}`),要求先转交或解散。
|
|
242430
|
+
* 🚨 这条检查天然跨组织、与「当前公司」无关——**不能**只看当前组织,漏一家就等于把那家组织
|
|
242431
|
+
* 甩成无人可管,而那个人已经注销、再也转交不了。
|
|
242432
|
+
*
|
|
242433
|
+
* 返回值 = 要逐家执行退出副作用的 membership 清单。**只含 active 组织**:已解散组织里的成员行
|
|
242434
|
+
* 原样保留(账号停用后进不去,解散组织本身也解析不出来),不为了「删干净」去碰一家已经归档的
|
|
242435
|
+
* 组织的成员名单——那会让它的历史成员名册凭空少人。
|
|
242436
|
+
*/
|
|
242437
|
+
async peekAccountDeletion(accountId) {
|
|
242438
|
+
const account = await this.store.getAccount(accountId);
|
|
242439
|
+
if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
|
|
242440
|
+
const owned = await this.listOwnedActiveCompanies(accountId);
|
|
242441
|
+
if (owned.length > 0) {
|
|
242442
|
+
throw new CompanyError(
|
|
242443
|
+
409,
|
|
242444
|
+
"OWNER_IN_ORGS",
|
|
242445
|
+
"\u4F60\u8FD8\u662F\u4EE5\u4E0B\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8BF7\u5148\u8F6C\u4EA4\u6216\u89E3\u6563\u540E\u518D\u6CE8\u9500",
|
|
242446
|
+
{ companies: owned.map((c) => ({ id: c.id, name: c.name })) }
|
|
242447
|
+
);
|
|
242448
|
+
}
|
|
242449
|
+
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
242450
|
+
const out = [];
|
|
242451
|
+
for (const m2 of memberships) {
|
|
242452
|
+
const c = await this.store.getCompany(m2.companyId);
|
|
242453
|
+
if (c?.status === "active") out.push(m2);
|
|
242454
|
+
}
|
|
242455
|
+
return out;
|
|
242456
|
+
}
|
|
242457
|
+
/**
|
|
242458
|
+
* 注销的最后一步:账号置 `disabled` + **bump `tokenVersion`**,当场作废该账号全部会话。
|
|
242459
|
+
* 这是 `tokenVersion` 的**正确**用法(账号级总闸);转交与降级**不得**用它——那会把人从
|
|
242460
|
+
* 其它组织一起踢下线(G10)。
|
|
242461
|
+
*
|
|
242462
|
+
* **数据口径要诚实**:oplog 里的历史署名、已交付产物、任务轨迹**不删**(内核纯净红线)。
|
|
242463
|
+
* 「注销」= 这个人再也登不进来、不再是任何 active 组织的成员,不等于「个人数据被永久删除」。
|
|
242464
|
+
*/
|
|
242465
|
+
async finalizeAccountDeletion(accountId) {
|
|
242466
|
+
const account = await this.store.getAccount(accountId);
|
|
242467
|
+
if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
|
|
242468
|
+
await this.store.upsertAccount({ ...account, status: "disabled" });
|
|
242469
|
+
await this.store.bumpAccountTokenVersion(accountId);
|
|
241832
242470
|
}
|
|
241833
242471
|
// —— 邀请(本地模型,CO-005)——
|
|
241834
242472
|
/**
|
|
@@ -242263,33 +242901,32 @@ var init_service6 = __esm({
|
|
|
242263
242901
|
if (!c) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
|
|
242264
242902
|
return c;
|
|
242265
242903
|
}
|
|
242266
|
-
async isLastOwner(companyId, accountId) {
|
|
242267
|
-
const owners = (await this.store.listMembers(companyId)).filter((m2) => m2.role === "owner");
|
|
242268
|
-
return owners.length === 1 && owners[0].accountId === accountId;
|
|
242269
|
-
}
|
|
242270
242904
|
};
|
|
242271
242905
|
}
|
|
242272
242906
|
});
|
|
242273
242907
|
|
|
242274
242908
|
// ../server/src/domains/companies/offboarding.ts
|
|
242275
|
-
var import_node_crypto59, MemberOffboardingService;
|
|
242909
|
+
var import_node_crypto59, OWNER_PROTECTED_MESSAGE, MemberOffboardingService;
|
|
242276
242910
|
var init_offboarding = __esm({
|
|
242277
242911
|
"../server/src/domains/companies/offboarding.ts"() {
|
|
242278
242912
|
"use strict";
|
|
242279
242913
|
import_node_crypto59 = require("node:crypto");
|
|
242280
242914
|
init_service6();
|
|
242915
|
+
OWNER_PROTECTED_MESSAGE = "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7";
|
|
242281
242916
|
MemberOffboardingService = class {
|
|
242282
242917
|
store;
|
|
242283
242918
|
now;
|
|
242284
242919
|
listInFlight;
|
|
242285
242920
|
transferAssignment;
|
|
242286
242921
|
disableActor;
|
|
242922
|
+
restoreActor;
|
|
242287
242923
|
constructor(opts) {
|
|
242288
242924
|
this.store = opts.store;
|
|
242289
242925
|
this.now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
242290
242926
|
this.listInFlight = opts.listInFlightAssignments;
|
|
242291
242927
|
this.transferAssignment = opts.transferAssignment;
|
|
242292
242928
|
this.disableActor = opts.disableActor;
|
|
242929
|
+
this.restoreActor = opts.restoreActor;
|
|
242293
242930
|
}
|
|
242294
242931
|
/** 该成员是不是公司最后一个 owner(最后一个 owner 立即阻断,先于任何交接讨论)。 */
|
|
242295
242932
|
async isLastOwner(companyId, accountId) {
|
|
@@ -242315,12 +242952,13 @@ var init_offboarding = __esm({
|
|
|
242315
242952
|
*/
|
|
242316
242953
|
async blockersForRemoval(companyId, accountId) {
|
|
242317
242954
|
const blockers = [];
|
|
242318
|
-
const
|
|
242955
|
+
const members = await this.store.listMembers(companyId);
|
|
242956
|
+
const member = members.find((m2) => m2.accountId === accountId);
|
|
242319
242957
|
if (!member) {
|
|
242320
242958
|
return [{ code: "NOT_FOUND", message: `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}` }];
|
|
242321
242959
|
}
|
|
242322
|
-
if (
|
|
242323
|
-
blockers.push({ code: "
|
|
242960
|
+
if (member.role === "owner") {
|
|
242961
|
+
blockers.push({ code: "OWNER_PROTECTED", message: OWNER_PROTECTED_MESSAGE });
|
|
242324
242962
|
}
|
|
242325
242963
|
return blockers;
|
|
242326
242964
|
}
|
|
@@ -242342,6 +242980,7 @@ var init_offboarding = __esm({
|
|
|
242342
242980
|
async plan(companyId, accountId, initiatedBy) {
|
|
242343
242981
|
const member = await this.store.getMember(companyId, accountId);
|
|
242344
242982
|
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242983
|
+
if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
242345
242984
|
if (await this.isLastOwner(companyId, accountId)) {
|
|
242346
242985
|
throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
|
|
242347
242986
|
}
|
|
@@ -242388,10 +243027,9 @@ var init_offboarding = __esm({
|
|
|
242388
243027
|
if (op.version !== expectedVersion) {
|
|
242389
243028
|
throw new CompanyError(409, "STALE_PLAN", `\u8BA1\u5212\u7248\u672C\u5DF2\u53D8\uFF08\u5F53\u524D ${op.version}\uFF0C\u4F60\u7ED9\u7684\u662F ${expectedVersion}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D6\u8BA1\u5212`);
|
|
242390
243029
|
}
|
|
242391
|
-
|
|
242392
|
-
|
|
242393
|
-
|
|
242394
|
-
const released = await this.releaseOwnedAgents(companyId, accountId);
|
|
243030
|
+
const members = await this.store.listMembers(companyId);
|
|
243031
|
+
const target = members.find((m2) => m2.accountId === accountId);
|
|
243032
|
+
if (target?.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
242395
243033
|
const remaining = await this.blockersForRemoval(companyId, accountId);
|
|
242396
243034
|
if (remaining.length > 0) {
|
|
242397
243035
|
await this.store.updateMemberOffboardingIfVersion(
|
|
@@ -242403,19 +243041,7 @@ var init_offboarding = __esm({
|
|
|
242403
243041
|
);
|
|
242404
243042
|
throw new CompanyError(409, remaining[0].code, `\u4ECD\u6709\u672A\u4EA4\u63A5\u9879\uFF1A${remaining.map((b2) => b2.message).join("; ")}`);
|
|
242405
243043
|
}
|
|
242406
|
-
const
|
|
242407
|
-
if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
|
|
242408
|
-
await this.disableActor?.(companyId, member.actorId);
|
|
242409
|
-
}
|
|
242410
|
-
await this.store.deletePrincipalBridge(companyId, "human", accountId);
|
|
242411
|
-
const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
|
|
242412
|
-
if (!removed.ok) {
|
|
242413
|
-
throw new CompanyError(
|
|
242414
|
-
removed.reason === "last_owner" ? 409 : 404,
|
|
242415
|
-
removed.reason === "last_owner" ? "LAST_OWNER" : "NOT_FOUND",
|
|
242416
|
-
removed.reason === "last_owner" ? "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner" : `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`
|
|
242417
|
-
);
|
|
242418
|
-
}
|
|
243044
|
+
const released = await this.applyMemberRemoval(companyId, accountId);
|
|
242419
243045
|
await this.store.updateMemberOffboardingIfVersion(
|
|
242420
243046
|
companyId,
|
|
242421
243047
|
operationId,
|
|
@@ -242425,6 +243051,65 @@ var init_offboarding = __esm({
|
|
|
242425
243051
|
);
|
|
242426
243052
|
return { status: "removed", transferred: [], left: released };
|
|
242427
243053
|
}
|
|
243054
|
+
/**
|
|
243055
|
+
* 移除一条成员关系的**全套副作用**,三条移除路径共用(管理员移除他人 `commit`、
|
|
243056
|
+
* 本人退出组织 `DELETE …/members/me`、注销账号 `DELETE /api/accounts/me`)。
|
|
243057
|
+
*
|
|
243058
|
+
* 顺序与原子性(2026-09-23 代码评审打回后重排):
|
|
243059
|
+
* ① 读一遍成员行:不存在 → 404;**已经是组织管理员 → 当场 403,一个副作用都不做**(D5.2)。
|
|
243060
|
+
* ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行)。
|
|
243061
|
+
* 这一步必须在③之前:成员行一删,本公司的 scoped registry 就不再认这个 actor,
|
|
243062
|
+
* 停用会静默 no-op,人被移除了却还挂在花名册上(QA D3 修过一次的病)。
|
|
243063
|
+
* ③ **一个事务**做完控制面三件事:锁内复核目标不是组织管理员 → 清名下 agent 的负责人字段
|
|
243064
|
+
* (`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,不清就是裸 FK 违例;
|
|
243065
|
+
* **只清 owner**,不改状态、不撤凭证、不解绑助理,PRD v1.5)→ 删 human principal bridge
|
|
243066
|
+
* → 删成员行(`store.removeMemberAndReleaseOwnedAgents`)。
|
|
243067
|
+
* ④ ③失败(并发转交把这个人升成了组织管理员,A09)→ 把②CAS 还原回去。
|
|
243068
|
+
*
|
|
243069
|
+
* **为什么③非要是一个事务**:以前①②③是「先清负责人、再停用、再删桥,最后才去拿锁删成员行」
|
|
243070
|
+
* 四段独立的写。A09 那条并发(`O` 转交给 `A1` 的同时另一位管理员删 `A1`)走到最后一步才被
|
|
243071
|
+
* `OWNER_PROTECTED` 拒——而此刻 `A1` 名下 Agent 的负责人已经被清空、桥已经被删、人已经被停用。
|
|
243072
|
+
* 请求失败了,破坏却留下了:新上任的组织管理员丢了名下全部 Agent 的归属、还从花名册上消失。
|
|
243073
|
+
* 现在控制面那三件事共一把锁(与转交锁同一组行),失败时一行都没写;跨不进这把锁的只有员工侧
|
|
243074
|
+
* 停用,用 CAS 补偿还原——「停用之后有别人写过这个 actor」就跳过,不撤销他人的合法写(QA R9)。
|
|
243075
|
+
*/
|
|
243076
|
+
async applyMemberRemoval(companyId, accountId) {
|
|
243077
|
+
const member = await this.store.getMember(companyId, accountId);
|
|
243078
|
+
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
243079
|
+
if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
243080
|
+
let ticket = null;
|
|
243081
|
+
if (!await isEmployedElsewhere(this.store, companyId, accountId)) {
|
|
243082
|
+
ticket = await this.disableActor?.(companyId, member.actorId) ?? null;
|
|
243083
|
+
}
|
|
243084
|
+
const removed = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now()).catch(async (e) => {
|
|
243085
|
+
await this.compensateDisable(companyId, ticket);
|
|
243086
|
+
throw e;
|
|
243087
|
+
});
|
|
243088
|
+
if (!removed.ok) {
|
|
243089
|
+
await this.compensateDisable(companyId, ticket);
|
|
243090
|
+
if (removed.reason === "not_found") {
|
|
243091
|
+
throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
243092
|
+
}
|
|
243093
|
+
throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
243094
|
+
}
|
|
243095
|
+
return removed.releasedAgents;
|
|
243096
|
+
}
|
|
243097
|
+
/**
|
|
243098
|
+
* 把②那次停用还原回去。**只还原本次自己造成的那一次**:`restoreActor` 走 CAS,
|
|
243099
|
+
* 停用之后但凡有别人写过这个 actor 就跳过——留下「成员还在 + actor 已停用」这个合法终态
|
|
243100
|
+
* (与员工页 `DELETE /api/actors/:id` 的终态同构),比静默撤销他人的合法停用安全。
|
|
243101
|
+
* 补偿本身失败不改写主错误:调用方要看到的是「为什么没删成」,不是补偿的二级故障。
|
|
243102
|
+
*/
|
|
243103
|
+
async compensateDisable(companyId, ticket) {
|
|
243104
|
+
if (!ticket || !this.restoreActor) return;
|
|
243105
|
+
try {
|
|
243106
|
+
await this.restoreActor(companyId, ticket);
|
|
243107
|
+
} catch (e) {
|
|
243108
|
+
console.error(
|
|
243109
|
+
`[offboarding] restore actor ${ticket.actorId} failed after removal was rejected: ${e instanceof Error ? e.message : String(e)}`
|
|
243110
|
+
);
|
|
243111
|
+
}
|
|
243112
|
+
}
|
|
242428
243113
|
/**
|
|
242429
243114
|
* 把该成员名下所有 agent 的负责人字段清空,返回被清的 agent id。
|
|
242430
243115
|
*
|
|
@@ -242512,13 +243197,28 @@ function companiesDomain(opts) {
|
|
|
242512
243197
|
}
|
|
242513
243198
|
return n;
|
|
242514
243199
|
};
|
|
243200
|
+
const requireActiveCompany = async (companyId) => {
|
|
243201
|
+
const company = await service.get(companyId);
|
|
243202
|
+
if (!company || company.status !== "active") {
|
|
243203
|
+
throw new ApiError(404, "COMPANY_NOT_FOUND", `\u7EC4\u7EC7\u4E0D\u5B58\u5728\u6216\u5DF2\u4E0D\u53EF\u7528\uFF1A${companyId}`);
|
|
243204
|
+
}
|
|
243205
|
+
};
|
|
242515
243206
|
const requireOwnerOrAdmin = async (req, companyId) => {
|
|
242516
243207
|
const me = await whoami(req);
|
|
242517
243208
|
const member = await service.getMember(companyId, me.accountId);
|
|
242518
|
-
if (!member
|
|
243209
|
+
if (!member) throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
|
|
243210
|
+
await requireActiveCompany(companyId);
|
|
243211
|
+
if (member.role !== "owner" && member.role !== "admin") {
|
|
242519
243212
|
throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
|
|
242520
243213
|
}
|
|
242521
243214
|
};
|
|
243215
|
+
const requireMember = async (req, companyId) => {
|
|
243216
|
+
const me = await whoami(req);
|
|
243217
|
+
const member = await service.getMember(companyId, me.accountId);
|
|
243218
|
+
if (!member) throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
|
|
243219
|
+
await requireActiveCompany(companyId);
|
|
243220
|
+
return me;
|
|
243221
|
+
};
|
|
242522
243222
|
return (router) => {
|
|
242523
243223
|
router.get("/api/companies", async (req) => {
|
|
242524
243224
|
const me = await whoami(req);
|
|
@@ -242578,7 +243278,9 @@ function companiesDomain(opts) {
|
|
|
242578
243278
|
return { status: 200, body: { ok: true } };
|
|
242579
243279
|
});
|
|
242580
243280
|
router.get("/api/companies/:id/members", async (req) => {
|
|
242581
|
-
const
|
|
243281
|
+
const cid = req.params.id;
|
|
243282
|
+
await requireMember(req, cid);
|
|
243283
|
+
const items = await service.listMemberViews(cid).catch(mapErr);
|
|
242582
243284
|
return { status: 200, body: { items } };
|
|
242583
243285
|
});
|
|
242584
243286
|
router.get("/api/companies/:id/members/me", async (req) => {
|
|
@@ -242608,6 +243310,9 @@ function companiesDomain(opts) {
|
|
|
242608
243310
|
if (b2.role === void 0 && b2.displayName === void 0) {
|
|
242609
243311
|
throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08role / displayName\uFF09");
|
|
242610
243312
|
}
|
|
243313
|
+
const me = await whoami(req);
|
|
243314
|
+
if (b2.role !== void 0 || me.accountId !== aid) await requireOwnerOrAdmin(req, cid);
|
|
243315
|
+
else await requireMember(req, cid);
|
|
242611
243316
|
let member;
|
|
242612
243317
|
if (b2.role !== void 0) {
|
|
242613
243318
|
if (!b2.role) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 role");
|
|
@@ -242622,11 +243327,19 @@ function companiesDomain(opts) {
|
|
|
242622
243327
|
}
|
|
242623
243328
|
return { status: 200, body: member };
|
|
242624
243329
|
});
|
|
243330
|
+
router.delete("/api/companies/:id/members/me", async (req) => {
|
|
243331
|
+
const off = requireOffboarding();
|
|
243332
|
+
const cid = req.params.id;
|
|
243333
|
+
const me = await whoami(req);
|
|
243334
|
+
await service.peekSelfLeave(cid, me.accountId).catch(mapErr);
|
|
243335
|
+
const released = await off.applyMemberRemoval(cid, me.accountId).catch(mapErr);
|
|
243336
|
+
return { status: 200, body: { ok: true, releasedAgents: released } };
|
|
243337
|
+
});
|
|
242625
243338
|
router.delete("/api/companies/:id/members/:accountId", async (req) => {
|
|
242626
243339
|
const cid = req.params.id;
|
|
242627
243340
|
const aid = req.params.accountId;
|
|
243341
|
+
await requireOwnerOrAdmin(req, cid);
|
|
242628
243342
|
const member = await service.peekRemovableMember(cid, aid).catch(mapErr);
|
|
242629
|
-
if (offboarding) await offboarding.releaseOwnedAgents(cid, aid).catch(mapErr);
|
|
242630
243343
|
let actorsSvc;
|
|
242631
243344
|
let ownedDisableBefore = null;
|
|
242632
243345
|
let disableSeq = 0;
|
|
@@ -242666,6 +243379,32 @@ function companiesDomain(opts) {
|
|
|
242666
243379
|
}
|
|
242667
243380
|
return { status: 200, body: { ok: true } };
|
|
242668
243381
|
});
|
|
243382
|
+
router.post("/api/companies/:id/owner", async (req) => {
|
|
243383
|
+
const cid = req.params.id;
|
|
243384
|
+
const b2 = req.body ?? {};
|
|
243385
|
+
if (typeof b2.toAccountId !== "string" || !b2.toAccountId.trim()) {
|
|
243386
|
+
throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId: string");
|
|
243387
|
+
}
|
|
243388
|
+
const me = await whoami(req);
|
|
243389
|
+
const res = await service.transferOwner(cid, me.accountId, b2.toAccountId.trim()).catch(mapErr);
|
|
243390
|
+
return { status: 200, body: res };
|
|
243391
|
+
});
|
|
243392
|
+
router.delete("/api/companies/:id", async (req) => {
|
|
243393
|
+
const cid = req.params.id;
|
|
243394
|
+
const me = await whoami(req);
|
|
243395
|
+
const company = await service.dissolveCompany(cid, me.accountId).catch(mapErr);
|
|
243396
|
+
return { status: 200, body: company };
|
|
243397
|
+
});
|
|
243398
|
+
router.delete("/api/accounts/me", async (req) => {
|
|
243399
|
+
const off = requireOffboarding();
|
|
243400
|
+
const me = await whoami(req);
|
|
243401
|
+
const memberships = await service.peekAccountDeletion(me.accountId).catch(mapErr);
|
|
243402
|
+
for (const m2 of memberships) {
|
|
243403
|
+
await off.applyMemberRemoval(m2.companyId, me.accountId).catch(mapErr);
|
|
243404
|
+
}
|
|
243405
|
+
await service.finalizeAccountDeletion(me.accountId).catch(mapErr);
|
|
243406
|
+
return { status: 200, body: { ok: true, leftCompanies: memberships.map((m2) => m2.companyId) } };
|
|
243407
|
+
});
|
|
242669
243408
|
router.get("/api/companies/:id/agent-credentials", async (req) => {
|
|
242670
243409
|
const creds = requireCredentials();
|
|
242671
243410
|
const cid = req.params.id;
|
|
@@ -242713,6 +243452,7 @@ function companiesDomain(opts) {
|
|
|
242713
243452
|
router.post("/api/companies/:id/invitations", async (req) => {
|
|
242714
243453
|
const b2 = req.body ?? {};
|
|
242715
243454
|
if (!b2.email) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 email");
|
|
243455
|
+
await requireOwnerOrAdmin(req, req.params.id);
|
|
242716
243456
|
const role = b2.role ?? "member";
|
|
242717
243457
|
const me = await whoami(req);
|
|
242718
243458
|
const invitation = await service.createInvitation(req.params.id, b2.email, role, me.accountId).catch(mapErr);
|
|
@@ -242852,7 +243592,7 @@ var init_routes6 = __esm({
|
|
|
242852
243592
|
init_router();
|
|
242853
243593
|
init_service6();
|
|
242854
243594
|
init_image_upload();
|
|
242855
|
-
COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links"];
|
|
243595
|
+
COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links", "/api/accounts"];
|
|
242856
243596
|
}
|
|
242857
243597
|
});
|
|
242858
243598
|
|
|
@@ -242995,9 +243735,30 @@ function createCompaniesDomain(opts) {
|
|
|
242995
243735
|
...opts.resolveActorsCtx !== void 0 ? {
|
|
242996
243736
|
// 未认领转 left 的 agent、以及 commit 移除掉的那位成员,都要把员工侧 ActorRecord
|
|
242997
243737
|
// 一并软停用——否则控制面说「已离开/已移除」、花名册还显示在岗,两个事实源当场分叉。
|
|
243738
|
+
//
|
|
243739
|
+
// 用 `disableActorIfCurrentlyActive` 而不是 `disableActor`,并把 `{before, seq}` 回给
|
|
243740
|
+
// offboarding:只有**真的把这个 actor 从 active 拉到 disabled** 的那一次调用才拿得到
|
|
243741
|
+
// 返回值,也才「拥有」这次状态变更;已被并发请求或员工页先停用的一律 ok:false,
|
|
243742
|
+
// 本次就不 own、移除失败也不补偿(QA R10 的判据,两条移除路径同一套)。
|
|
242998
243743
|
disableActor: async (companyId, actorId) => {
|
|
242999
243744
|
const ctx = await opts.resolveActorsCtx(companyId);
|
|
243000
|
-
await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
|
|
243745
|
+
const res = await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
|
|
243746
|
+
return res.ok ? { actorId, before: res.before, seq: res.seq } : null;
|
|
243747
|
+
},
|
|
243748
|
+
// 移除最终失败时的补偿:**CAS** 写回停用前那一行。`expectedSeq` 对不上(停用之后
|
|
243749
|
+
// 有任何第三方写过这个 actor)就跳过——不静默撤销他人的合法停用 / 改名(QA R9)。
|
|
243750
|
+
restoreActor: async (companyId, ticket) => {
|
|
243751
|
+
const ctx = await opts.resolveActorsCtx(companyId);
|
|
243752
|
+
const res = await ctx.service.casUpsertActor(
|
|
243753
|
+
{ ...ticket.before, status: "active" },
|
|
243754
|
+
ticket.seq,
|
|
243755
|
+
"system:offboarding"
|
|
243756
|
+
);
|
|
243757
|
+
if (!res.ok) {
|
|
243758
|
+
console.info(
|
|
243759
|
+
`[companies] restore skipped for ${ticket.actorId}: another write intervened after the disable`
|
|
243760
|
+
);
|
|
243761
|
+
}
|
|
243001
243762
|
}
|
|
243002
243763
|
} : {}
|
|
243003
243764
|
});
|
|
@@ -259991,7 +260752,35 @@ var init_pool = __esm({
|
|
|
259991
260752
|
});
|
|
259992
260753
|
|
|
259993
260754
|
// ../storage/src/postgres-control-plane.ts
|
|
259994
|
-
|
|
260755
|
+
async function ensureOneOwnerInvariant(pool, s2) {
|
|
260756
|
+
try {
|
|
260757
|
+
const bad = await pool.query(
|
|
260758
|
+
`SELECT company_id, count(*) FILTER (WHERE role = 'owner') AS owners
|
|
260759
|
+
FROM "${s2}".company_members
|
|
260760
|
+
GROUP BY company_id
|
|
260761
|
+
HAVING count(*) FILTER (WHERE role = 'owner') <> 1
|
|
260762
|
+
ORDER BY company_id`
|
|
260763
|
+
);
|
|
260764
|
+
if (bad.rowCount && bad.rowCount > 0) {
|
|
260765
|
+
console.warn(
|
|
260766
|
+
`[control-plane] ${bad.rowCount} \u5BB6\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u4E0D\u662F 1\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u4E0D\u81EA\u52A8\u6311\u4EBA\uFF09\uFF1A` + bad.rows.map((r) => `${r.company_id}=${r.owners}`).join(", ") + `\u3002\u7528 \`pnpm migrate:org-identity inventory\` \u51FA\u76D8\u70B9\u3001\u6309\u51BB\u7ED3 manifest \u8DD1 backfill\u3002`
|
|
260767
|
+
);
|
|
260768
|
+
}
|
|
260769
|
+
} catch (e) {
|
|
260770
|
+
console.warn(`[control-plane] \u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u590D\u6838\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
260771
|
+
}
|
|
260772
|
+
try {
|
|
260773
|
+
await pool.query(
|
|
260774
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS company_members_one_owner_idx
|
|
260775
|
+
ON "${s2}".company_members (company_id) WHERE role = 'owner'`
|
|
260776
|
+
);
|
|
260777
|
+
} catch (e) {
|
|
260778
|
+
console.warn(
|
|
260779
|
+
`[control-plane] \u5EFA company_members_one_owner_idx \u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF0C\u5E94\u7528\u5C42\u5224\u636E\u4ECD\u5728\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`
|
|
260780
|
+
);
|
|
260781
|
+
}
|
|
260782
|
+
}
|
|
260783
|
+
var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, migrationRow2, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
|
|
259995
260784
|
var init_postgres_control_plane = __esm({
|
|
259996
260785
|
"../storage/src/postgres-control-plane.ts"() {
|
|
259997
260786
|
"use strict";
|
|
@@ -260021,6 +260810,8 @@ var init_postgres_control_plane = __esm({
|
|
|
260021
260810
|
onboarding_variant text -- \u767B\u5F55\u540E\u52A9\u7406\u5F15\u5BFC\u53D8\u4F53 A|B\uFF1BNULL=\u7F3A\u7701\u89C6\u4E3A A
|
|
260022
260811
|
)`);
|
|
260023
260812
|
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS onboarding_variant text`);
|
|
260813
|
+
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_at timestamptz`);
|
|
260814
|
+
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_by text`);
|
|
260024
260815
|
await pool.query(`
|
|
260025
260816
|
CREATE TABLE IF NOT EXISTS "${s2}".company_members (
|
|
260026
260817
|
company_id text NOT NULL,
|
|
@@ -260209,6 +261000,19 @@ var init_postgres_control_plane = __esm({
|
|
|
260209
261000
|
CHECK (status IN ('handoff_required','handoff_in_progress','ready_to_remove','removed'))
|
|
260210
261001
|
)`);
|
|
260211
261002
|
await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS member_offboardings_active_uq ON "${s2}".member_offboardings (company_id, account_id) WHERE status <> 'removed'`);
|
|
261003
|
+
await pool.query(`
|
|
261004
|
+
CREATE TABLE IF NOT EXISTS "${s2}".company_member_role_migrations (
|
|
261005
|
+
id bigserial PRIMARY KEY,
|
|
261006
|
+
company_id text NOT NULL,
|
|
261007
|
+
account_id text NOT NULL,
|
|
261008
|
+
role_before text NOT NULL,
|
|
261009
|
+
role_after text NOT NULL,
|
|
261010
|
+
evidence text NOT NULL, -- 'E1' | 'manifest'
|
|
261011
|
+
manifest_version text,
|
|
261012
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
261013
|
+
)`);
|
|
261014
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS company_member_role_migrations_company_idx ON "${s2}".company_member_role_migrations (company_id, applied_at DESC)`);
|
|
261015
|
+
await ensureOneOwnerInvariant(pool, s2);
|
|
260212
261016
|
return new _PostgresControlPlaneStore(pool, schema);
|
|
260213
261017
|
}
|
|
260214
261018
|
// —— 公司 ——
|
|
@@ -260249,11 +261053,51 @@ var init_postgres_control_plane = __esm({
|
|
|
260249
261053
|
args.push(patch.onboardingVariant);
|
|
260250
261054
|
sets.push(`onboarding_variant = $${args.length}`);
|
|
260251
261055
|
}
|
|
261056
|
+
if (patch.dissolvedAt !== void 0) {
|
|
261057
|
+
args.push(patch.dissolvedAt);
|
|
261058
|
+
sets.push(`dissolved_at = $${args.length}`);
|
|
261059
|
+
}
|
|
261060
|
+
if (patch.dissolvedBy !== void 0) {
|
|
261061
|
+
args.push(patch.dissolvedBy);
|
|
261062
|
+
sets.push(`dissolved_by = $${args.length}`);
|
|
261063
|
+
}
|
|
260252
261064
|
args.push(id);
|
|
260253
261065
|
const sql = sets.length ? `UPDATE ${this.s}.companies SET ${sets.join(", ")} WHERE id = $${args.length}` : `SELECT 1 FROM ${this.s}.companies WHERE id = $${args.length}`;
|
|
260254
261066
|
const r = await this.pool.query(sql, args);
|
|
260255
261067
|
if (r.rowCount === 0) throw new Error(`company not found: ${id}`);
|
|
260256
261068
|
}
|
|
261069
|
+
async dissolveCompanyIfOwner(companyId, accountId, at2) {
|
|
261070
|
+
const client = await this.pool.connect();
|
|
261071
|
+
try {
|
|
261072
|
+
await client.query("BEGIN");
|
|
261073
|
+
const caller = await client.query(
|
|
261074
|
+
`SELECT role FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2 FOR UPDATE`,
|
|
261075
|
+
[companyId, accountId]
|
|
261076
|
+
);
|
|
261077
|
+
if (!caller.rows[0] || caller.rows[0].role !== "owner") {
|
|
261078
|
+
await client.query("ROLLBACK");
|
|
261079
|
+
const exists = await this.pool.query(`SELECT 1 FROM ${this.s}.companies WHERE id = $1`, [companyId]);
|
|
261080
|
+
return { ok: false, reason: exists.rowCount ? "not_owner" : "not_found" };
|
|
261081
|
+
}
|
|
261082
|
+
const updated = await client.query(
|
|
261083
|
+
`UPDATE ${this.s}.companies SET status = 'suspended', dissolved_at = $2, dissolved_by = $3
|
|
261084
|
+
WHERE id = $1 RETURNING *`,
|
|
261085
|
+
[companyId, at2, accountId]
|
|
261086
|
+
);
|
|
261087
|
+
if (!updated.rows[0]) {
|
|
261088
|
+
await client.query("ROLLBACK");
|
|
261089
|
+
return { ok: false, reason: "not_found" };
|
|
261090
|
+
}
|
|
261091
|
+
await client.query("COMMIT");
|
|
261092
|
+
return { ok: true, company: rowToCompany(updated.rows[0]) };
|
|
261093
|
+
} catch (e) {
|
|
261094
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261095
|
+
});
|
|
261096
|
+
throw e;
|
|
261097
|
+
} finally {
|
|
261098
|
+
client.release();
|
|
261099
|
+
}
|
|
261100
|
+
}
|
|
260257
261101
|
// —— 成员 ——
|
|
260258
261102
|
async upsertMember(m2) {
|
|
260259
261103
|
await this.pool.query(
|
|
@@ -260327,6 +261171,48 @@ var init_postgres_control_plane = __esm({
|
|
|
260327
261171
|
client.release();
|
|
260328
261172
|
}
|
|
260329
261173
|
}
|
|
261174
|
+
async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
|
|
261175
|
+
const client = await this.pool.connect();
|
|
261176
|
+
try {
|
|
261177
|
+
await client.query("BEGIN");
|
|
261178
|
+
const all2 = await client.query(
|
|
261179
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
261180
|
+
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
261181
|
+
[companyId]
|
|
261182
|
+
);
|
|
261183
|
+
const target = all2.rows.map(rowToMember).find((m2) => m2.accountId === accountId);
|
|
261184
|
+
if (!target) {
|
|
261185
|
+
await client.query("ROLLBACK");
|
|
261186
|
+
return { ok: false, reason: "not_found" };
|
|
261187
|
+
}
|
|
261188
|
+
if (target.role === "owner") {
|
|
261189
|
+
await client.query("ROLLBACK");
|
|
261190
|
+
return { ok: false, reason: "owner_protected" };
|
|
261191
|
+
}
|
|
261192
|
+
const released = await client.query(
|
|
261193
|
+
`UPDATE ${this.s}.digital_employees SET owner_account_id = NULL, updated_at = $3
|
|
261194
|
+
WHERE company_id = $1 AND owner_account_id = $2
|
|
261195
|
+
RETURNING id`,
|
|
261196
|
+
[companyId, accountId, at2]
|
|
261197
|
+
);
|
|
261198
|
+
await client.query(
|
|
261199
|
+
`DELETE FROM ${this.s}.principal_bridge WHERE company_id = $1 AND principal_type = 'human' AND principal_id = $2`,
|
|
261200
|
+
[companyId, accountId]
|
|
261201
|
+
);
|
|
261202
|
+
await client.query(
|
|
261203
|
+
`DELETE FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2`,
|
|
261204
|
+
[companyId, accountId]
|
|
261205
|
+
);
|
|
261206
|
+
await client.query("COMMIT");
|
|
261207
|
+
return { ok: true, member: target, releasedAgents: released.rows.map((r) => r.id) };
|
|
261208
|
+
} catch (e) {
|
|
261209
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261210
|
+
});
|
|
261211
|
+
throw e;
|
|
261212
|
+
} finally {
|
|
261213
|
+
client.release();
|
|
261214
|
+
}
|
|
261215
|
+
}
|
|
260330
261216
|
async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
|
|
260331
261217
|
const client = await this.pool.connect();
|
|
260332
261218
|
try {
|
|
@@ -260365,6 +261251,170 @@ var init_postgres_control_plane = __esm({
|
|
|
260365
261251
|
client.release();
|
|
260366
261252
|
}
|
|
260367
261253
|
}
|
|
261254
|
+
async setSoleCompanyOwner(companyId, toAccountId, opts) {
|
|
261255
|
+
const client = await this.pool.connect();
|
|
261256
|
+
try {
|
|
261257
|
+
await client.query("BEGIN");
|
|
261258
|
+
const all2 = await client.query(
|
|
261259
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
261260
|
+
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
261261
|
+
[companyId]
|
|
261262
|
+
);
|
|
261263
|
+
const rows = all2.rows.map(rowToMember);
|
|
261264
|
+
const target = rows.find((m2) => m2.accountId === toAccountId);
|
|
261265
|
+
if (!target) {
|
|
261266
|
+
await client.query("ROLLBACK");
|
|
261267
|
+
return { ok: false, reason: "not_found" };
|
|
261268
|
+
}
|
|
261269
|
+
if (opts?.requireCurrentOwner !== void 0) {
|
|
261270
|
+
const caller = rows.find((m2) => m2.accountId === opts.requireCurrentOwner);
|
|
261271
|
+
if (!caller || caller.role !== "owner") {
|
|
261272
|
+
await client.query("ROLLBACK");
|
|
261273
|
+
return { ok: false, reason: "not_owner" };
|
|
261274
|
+
}
|
|
261275
|
+
}
|
|
261276
|
+
if (!target.onboardingCompletedAt && target.role !== "owner") {
|
|
261277
|
+
await client.query("ROLLBACK");
|
|
261278
|
+
return { ok: false, reason: "target_not_listed" };
|
|
261279
|
+
}
|
|
261280
|
+
const acc = await client.query(
|
|
261281
|
+
`SELECT status FROM ${this.s}.accounts WHERE id = $1`,
|
|
261282
|
+
[toAccountId]
|
|
261283
|
+
);
|
|
261284
|
+
if (!acc.rows[0] || acc.rows[0].status !== "active") {
|
|
261285
|
+
await client.query("ROLLBACK");
|
|
261286
|
+
return { ok: false, reason: "target_inactive" };
|
|
261287
|
+
}
|
|
261288
|
+
const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
|
|
261289
|
+
if (others.length === 0 && target.role === "owner") {
|
|
261290
|
+
await client.query("COMMIT");
|
|
261291
|
+
return { ok: true, owner: target, demoted: [] };
|
|
261292
|
+
}
|
|
261293
|
+
if (others.length > 0) {
|
|
261294
|
+
await client.query(
|
|
261295
|
+
`UPDATE ${this.s}.company_members SET role = 'admin', updated_at = now()
|
|
261296
|
+
WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
|
|
261297
|
+
[companyId, toAccountId]
|
|
261298
|
+
);
|
|
261299
|
+
}
|
|
261300
|
+
if (target.role !== "owner") {
|
|
261301
|
+
await client.query(
|
|
261302
|
+
`UPDATE ${this.s}.company_members SET role = 'owner', updated_at = now()
|
|
261303
|
+
WHERE company_id = $1 AND account_id = $2`,
|
|
261304
|
+
[companyId, toAccountId]
|
|
261305
|
+
);
|
|
261306
|
+
}
|
|
261307
|
+
await client.query("COMMIT");
|
|
261308
|
+
return {
|
|
261309
|
+
ok: true,
|
|
261310
|
+
owner: { ...target, role: "owner" },
|
|
261311
|
+
demoted: others.map((m2) => ({ ...m2, role: "admin" }))
|
|
261312
|
+
};
|
|
261313
|
+
} catch (e) {
|
|
261314
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261315
|
+
});
|
|
261316
|
+
throw e;
|
|
261317
|
+
} finally {
|
|
261318
|
+
client.release();
|
|
261319
|
+
}
|
|
261320
|
+
}
|
|
261321
|
+
// —— 存量身份映射(ADR D3)——
|
|
261322
|
+
async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
|
|
261323
|
+
const client = await this.pool.connect();
|
|
261324
|
+
try {
|
|
261325
|
+
await client.query("BEGIN");
|
|
261326
|
+
const all2 = await client.query(
|
|
261327
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
261328
|
+
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
261329
|
+
[companyId]
|
|
261330
|
+
);
|
|
261331
|
+
const rows = all2.rows.map(rowToMember);
|
|
261332
|
+
const target = rows.find((m2) => m2.accountId === toAccountId);
|
|
261333
|
+
if (!target) {
|
|
261334
|
+
await client.query("ROLLBACK");
|
|
261335
|
+
return { ok: false, reason: "not_found" };
|
|
261336
|
+
}
|
|
261337
|
+
const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
|
|
261338
|
+
if (others.length === 0 && target.role === "owner") {
|
|
261339
|
+
await client.query("COMMIT");
|
|
261340
|
+
return { ok: true, owner: target, demoted: [], auditRows: [] };
|
|
261341
|
+
}
|
|
261342
|
+
if (others.length > 0) {
|
|
261343
|
+
await client.query(
|
|
261344
|
+
`UPDATE ${this.s}.company_members SET role = 'admin' WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
|
|
261345
|
+
[companyId, toAccountId]
|
|
261346
|
+
);
|
|
261347
|
+
}
|
|
261348
|
+
if (target.role !== "owner") {
|
|
261349
|
+
await client.query(
|
|
261350
|
+
`UPDATE ${this.s}.company_members SET role = 'owner' WHERE company_id = $1 AND account_id = $2`,
|
|
261351
|
+
[companyId, toAccountId]
|
|
261352
|
+
);
|
|
261353
|
+
}
|
|
261354
|
+
const auditRows = [
|
|
261355
|
+
...others.map((m2) => migrationRow2(companyId, m2.accountId, "owner", "admin", audit2)),
|
|
261356
|
+
...target.role !== "owner" ? [migrationRow2(companyId, toAccountId, target.role, "owner", audit2)] : []
|
|
261357
|
+
];
|
|
261358
|
+
for (const r of auditRows) {
|
|
261359
|
+
await client.query(
|
|
261360
|
+
`INSERT INTO ${this.s}.company_member_role_migrations
|
|
261361
|
+
(company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
|
|
261362
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
261363
|
+
[r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
|
|
261364
|
+
);
|
|
261365
|
+
}
|
|
261366
|
+
await client.query("COMMIT");
|
|
261367
|
+
return {
|
|
261368
|
+
ok: true,
|
|
261369
|
+
owner: { ...target, role: "owner" },
|
|
261370
|
+
demoted: others.map((m2) => ({ ...m2, role: "admin" })),
|
|
261371
|
+
auditRows
|
|
261372
|
+
};
|
|
261373
|
+
} catch (e) {
|
|
261374
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261375
|
+
});
|
|
261376
|
+
throw e;
|
|
261377
|
+
} finally {
|
|
261378
|
+
client.release();
|
|
261379
|
+
}
|
|
261380
|
+
}
|
|
261381
|
+
async setMemberRoleOnly(companyId, accountId, role) {
|
|
261382
|
+
const r = await this.pool.query(
|
|
261383
|
+
`UPDATE ${this.s}.company_members SET role = $3 WHERE company_id = $1 AND account_id = $2`,
|
|
261384
|
+
[companyId, accountId, role]
|
|
261385
|
+
);
|
|
261386
|
+
return (r.rowCount ?? 0) > 0;
|
|
261387
|
+
}
|
|
261388
|
+
// —— 存量身份映射审计(ADR D3.4)——
|
|
261389
|
+
async appendMemberRoleMigrations(rows) {
|
|
261390
|
+
if (rows.length === 0) return;
|
|
261391
|
+
const client = await this.pool.connect();
|
|
261392
|
+
try {
|
|
261393
|
+
await client.query("BEGIN");
|
|
261394
|
+
for (const r of rows) {
|
|
261395
|
+
await client.query(
|
|
261396
|
+
`INSERT INTO ${this.s}.company_member_role_migrations
|
|
261397
|
+
(company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
|
|
261398
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
261399
|
+
[r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
|
|
261400
|
+
);
|
|
261401
|
+
}
|
|
261402
|
+
await client.query("COMMIT");
|
|
261403
|
+
} catch (e) {
|
|
261404
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261405
|
+
});
|
|
261406
|
+
throw e;
|
|
261407
|
+
} finally {
|
|
261408
|
+
client.release();
|
|
261409
|
+
}
|
|
261410
|
+
}
|
|
261411
|
+
async listMemberRoleMigrations(companyId) {
|
|
261412
|
+
const r = companyId === void 0 ? await this.pool.query(`SELECT * FROM ${this.s}.company_member_role_migrations ORDER BY applied_at, id`) : await this.pool.query(
|
|
261413
|
+
`SELECT * FROM ${this.s}.company_member_role_migrations WHERE company_id = $1 ORDER BY applied_at, id`,
|
|
261414
|
+
[companyId]
|
|
261415
|
+
);
|
|
261416
|
+
return r.rows.map(rowToRoleMigration);
|
|
261417
|
+
}
|
|
260368
261418
|
// —— 邀请 ——
|
|
260369
261419
|
async createInvitation(inv) {
|
|
260370
261420
|
await this.pool.query(
|
|
@@ -260834,7 +261884,28 @@ var init_postgres_control_plane = __esm({
|
|
|
260834
261884
|
...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
|
|
260835
261885
|
status: row.status,
|
|
260836
261886
|
createdAt: new Date(row.created_at).toISOString(),
|
|
260837
|
-
...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {}
|
|
261887
|
+
...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {},
|
|
261888
|
+
// 解散痕迹(组织三层身份 ADR D9.2):两列一起有、一起空。普通运维停用不写它们。
|
|
261889
|
+
...row.dissolved_at != null ? { dissolvedAt: toIso(row.dissolved_at) } : {},
|
|
261890
|
+
...row.dissolved_by != null ? { dissolvedBy: row.dissolved_by } : {}
|
|
261891
|
+
});
|
|
261892
|
+
rowToRoleMigration = (row) => ({
|
|
261893
|
+
companyId: row.company_id,
|
|
261894
|
+
accountId: row.account_id,
|
|
261895
|
+
roleBefore: row.role_before,
|
|
261896
|
+
roleAfter: row.role_after,
|
|
261897
|
+
evidence: row.evidence,
|
|
261898
|
+
...row.manifest_version != null ? { manifestVersion: row.manifest_version } : {},
|
|
261899
|
+
appliedAt: toIso(row.applied_at)
|
|
261900
|
+
});
|
|
261901
|
+
migrationRow2 = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
|
|
261902
|
+
companyId,
|
|
261903
|
+
accountId,
|
|
261904
|
+
roleBefore,
|
|
261905
|
+
roleAfter,
|
|
261906
|
+
evidence: audit2.evidence,
|
|
261907
|
+
...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
|
|
261908
|
+
appliedAt: audit2.appliedAt
|
|
260838
261909
|
});
|
|
260839
261910
|
rowToMember = (row) => ({
|
|
260840
261911
|
companyId: row.company_id,
|
|
@@ -276559,6 +277630,7 @@ __export(src_exports2, {
|
|
|
276559
277630
|
ACTOR_DELETED_CHAT_MESSAGE: () => ACTOR_DELETED_CHAT_MESSAGE,
|
|
276560
277631
|
ACTOR_DELETED_ERROR_CODE: () => ACTOR_DELETED_ERROR_CODE,
|
|
276561
277632
|
ASSISTANT_ARCHIVED_CODE: () => ASSISTANT_ARCHIVED_CODE,
|
|
277633
|
+
ASSISTANT_POSITION: () => ASSISTANT_POSITION,
|
|
276562
277634
|
ASSISTANT_TIER_RULES: () => ASSISTANT_TIER_RULES,
|
|
276563
277635
|
AUTOMATION_TEMPLATES: () => AUTOMATION_TEMPLATES,
|
|
276564
277636
|
AUTONOMOUS_ETHOS: () => AUTONOMOUS_ETHOS,
|
|
@@ -276566,6 +277638,7 @@ __export(src_exports2, {
|
|
|
276566
277638
|
ActorMemoryService: () => ActorMemoryService,
|
|
276567
277639
|
ActorsService: () => ActorsService,
|
|
276568
277640
|
AgentCredentialService: () => AgentCredentialService,
|
|
277641
|
+
AgentManageForbiddenError: () => AgentManageForbiddenError,
|
|
276569
277642
|
ApiError: () => ApiError,
|
|
276570
277643
|
AssistantLazyCreateError: () => AssistantLazyCreateError,
|
|
276571
277644
|
AssistantsService: () => AssistantsService,
|
|
@@ -276703,6 +277776,7 @@ __export(src_exports2, {
|
|
|
276703
277776
|
OPERATOR_SCOPE: () => OPERATOR_SCOPE,
|
|
276704
277777
|
ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
|
|
276705
277778
|
OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
|
|
277779
|
+
OWNER_PROTECTED_MESSAGE: () => OWNER_PROTECTED_MESSAGE,
|
|
276706
277780
|
PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
|
|
276707
277781
|
PARENT_CONVERSATION_RECENT_TURNS: () => PARENT_CONVERSATION_RECENT_TURNS,
|
|
276708
277782
|
PostgresNotificationStore: () => PostgresNotificationStore,
|
|
@@ -276757,11 +277831,16 @@ __export(src_exports2, {
|
|
|
276757
277831
|
applyWorkspaceBundle: () => applyWorkspaceBundle,
|
|
276758
277832
|
assembleDelegationsDomain: () => assembleDelegationsDomain,
|
|
276759
277833
|
assertBindingWritePermission: () => assertBindingWritePermission,
|
|
277834
|
+
assertCompanyManager: () => assertCompanyManager,
|
|
276760
277835
|
assertCreatableVariable: () => assertCreatableVariable,
|
|
277836
|
+
assertMayCreateAgent: () => assertMayCreateAgent,
|
|
277837
|
+
assertMayManageAgent: () => assertMayManageAgent,
|
|
277838
|
+
assertMaySelfOrManage: () => assertMaySelfOrManage,
|
|
276761
277839
|
assistantRowRecovery: () => assistantRowRecovery,
|
|
276762
277840
|
assistantsDomain: () => assistantsDomain,
|
|
276763
277841
|
atCapacity: () => atCapacity,
|
|
276764
277842
|
automationsDomain: () => automationsDomain,
|
|
277843
|
+
backfillOrgIdentity: () => backfillOrgIdentity,
|
|
276765
277844
|
backfillOrgModel: () => backfillOrgModel,
|
|
276766
277845
|
backfillOrgModelOnStartup: () => backfillOrgModelOnStartup,
|
|
276767
277846
|
backfillResolvedLetterReadMarkers: () => backfillResolvedLetterReadMarkers,
|
|
@@ -276934,8 +278013,11 @@ __export(src_exports2, {
|
|
|
276934
278013
|
hideDeleted: () => hideDeleted,
|
|
276935
278014
|
humanSubmitSourceRunId: () => humanSubmitSourceRunId,
|
|
276936
278015
|
idlenessOf: () => idlenessOf,
|
|
278016
|
+
inventoryOrgIdentity: () => inventoryOrgIdentity,
|
|
276937
278017
|
isActiveCoordinatorAgent: () => isActiveCoordinatorAgent,
|
|
278018
|
+
isAssistantAgent: () => isAssistantAgent,
|
|
276938
278019
|
isCompanyCrossTenantPath: () => isCompanyCrossTenantPath,
|
|
278020
|
+
isCompanyManager: () => isCompanyManager,
|
|
276939
278021
|
isCoordinatorAgentName: () => isCoordinatorAgentName,
|
|
276940
278022
|
isCoordinatorRunReader: () => isCoordinatorRunReader,
|
|
276941
278023
|
isCredentialAuditKind: () => isCredentialAuditKind,
|
|
@@ -276995,6 +278077,7 @@ __export(src_exports2, {
|
|
|
276995
278077
|
matchedKnowledgeQueryTerms: () => matchedKnowledgeQueryTerms,
|
|
276996
278078
|
materializeBuiltinSkills: () => materializeBuiltinSkills,
|
|
276997
278079
|
materializeSkillFiles: () => materializeSkillFiles,
|
|
278080
|
+
mayManageAgent: () => mayManageAgent,
|
|
276998
278081
|
mayManageWorkorder: () => mayManageWorkorder,
|
|
276999
278082
|
mayReassignProjectItem: () => mayReassignProjectItem,
|
|
277000
278083
|
memberNameResolver: () => memberNameResolver,
|
|
@@ -277109,6 +278192,7 @@ __export(src_exports2, {
|
|
|
277109
278192
|
retireCdGateInTypeDef: () => retireCdGateInTypeDef,
|
|
277110
278193
|
retireCdGateInTypeStore: () => retireCdGateInTypeStore,
|
|
277111
278194
|
rolesDomain: () => rolesDomain,
|
|
278195
|
+
rollbackOrgIdentity: () => rollbackOrgIdentity,
|
|
277112
278196
|
routeFusedSignal: () => routeFusedSignal,
|
|
277113
278197
|
runtimeConnectionStatus: () => runtimeConnectionStatus,
|
|
277114
278198
|
runtimeDriftBlock: () => runtimeDriftBlock,
|
|
@@ -277200,6 +278284,7 @@ var init_src11 = __esm({
|
|
|
277200
278284
|
init_outputs_scan();
|
|
277201
278285
|
init_outputs_copy();
|
|
277202
278286
|
init_org_model_backfill();
|
|
278287
|
+
init_org_identity_migration();
|
|
277203
278288
|
init_types_admin();
|
|
277204
278289
|
init_roles_admin();
|
|
277205
278290
|
init_roles_seed();
|
|
@@ -277311,6 +278396,7 @@ var init_src11 = __esm({
|
|
|
277311
278396
|
init_chat_builtin_skills();
|
|
277312
278397
|
init_connector_skills();
|
|
277313
278398
|
init_connector_tool_versions();
|
|
278399
|
+
init_agent_manage_guard();
|
|
277314
278400
|
init_board_watch();
|
|
277315
278401
|
init_uncategorized_project();
|
|
277316
278402
|
init_run_settlement();
|
|
@@ -280989,6 +282075,28 @@ async function startServe(opts) {
|
|
|
280989
282075
|
const role = members.find((m2) => m2.actorId === actorId)?.role;
|
|
280990
282076
|
return role === "owner" || role === "admin" || role === "member" ? role : null;
|
|
280991
282077
|
},
|
|
282078
|
+
/* Agent 写面的行政权闸(组织三层身份 ADR D6,判据在 `governance/agent-manage-guard.ts`)。
|
|
282079
|
+
三条读取面都**每请求现查**、都按 `(组织, 账号)` 双键——禁止把角色塞进任何跨请求缓存
|
|
282080
|
+
(D7.1:`agentPrincipalByToken` 只缓存 `{companyId, actorId}`,不许顺手加 role)。
|
|
282081
|
+
`controlPlaneStore` / `getEngine` / `resolveAssistantsCtx` 在下方才就绪;这三个箭头只在
|
|
282082
|
+
请求时跑(同 `projectExists` 的惰性模式)。 */
|
|
282083
|
+
agentManageGuard: {
|
|
282084
|
+
callerRole: async (companyId, callerActorId) => {
|
|
282085
|
+
const cid = companyId ?? defaultCompanyId;
|
|
282086
|
+
const members = await controlPlaneStore.listMembers(cid);
|
|
282087
|
+
const row = members.find((m2) => m2.actorId === callerActorId || m2.accountId === callerActorId);
|
|
282088
|
+
return row?.role;
|
|
282089
|
+
},
|
|
282090
|
+
boundHuman: async (companyId, agentId) => {
|
|
282091
|
+
const ctx = await resolveAssistantsCtxLazy(companyId);
|
|
282092
|
+
if (!ctx) return null;
|
|
282093
|
+
return (await ctx.service.getByAgent(agentId))?.humanActorId ?? null;
|
|
282094
|
+
},
|
|
282095
|
+
reportsTo: async (companyId, humanActorId) => {
|
|
282096
|
+
const engine2 = await getEngine(companyId ?? defaultCompanyId);
|
|
282097
|
+
return (await engine2.registry.getActor(humanActorId))?.reportsTo ?? null;
|
|
282098
|
+
}
|
|
282099
|
+
},
|
|
280992
282100
|
// CO-302 数据面隔离(actors 域试点):按当前公司取其引擎,用各自 registry 服务该域请求。
|
|
280993
282101
|
// engineRouter 在下方(行 ~411)以 const 声明;此箭头只在请求时调用,那时已初始化,闭包引用合法
|
|
280994
282102
|
// (TDZ 只在构造时访问才报错,这里不访问)。默认公司命中现有单实例(registry===registryStore),
|
|
@@ -281051,6 +282159,12 @@ async function startServe(opts) {
|
|
|
281051
282159
|
isPlatformAdmin: async () => false
|
|
281052
282160
|
// 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
|
|
281053
282161
|
});
|
|
282162
|
+
let resolveAssistantsCtxImpl = null;
|
|
282163
|
+
const resolveAssistantsCtx = async (companyId) => {
|
|
282164
|
+
if (!resolveAssistantsCtxImpl) throw new Error("assistants ctx resolver \u5C1A\u672A\u88C5\u914D\uFF08\u542F\u52A8\u671F\u4E0D\u5E94\u6709\u8BF7\u6C42\uFF09");
|
|
282165
|
+
return resolveAssistantsCtxImpl(companyId);
|
|
282166
|
+
};
|
|
282167
|
+
const resolveAssistantsCtxLazy = async (companyId) => resolveAssistantsCtxImpl ? resolveAssistantsCtxImpl(companyId) : null;
|
|
281054
282168
|
let projectStateStore;
|
|
281055
282169
|
let artifactStateStore;
|
|
281056
282170
|
let chatSessionStore;
|
|
@@ -281780,8 +282894,7 @@ async function startServe(opts) {
|
|
|
281780
282894
|
const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, sharedSchema) : await FileControlPlaneStore.open(path30.join(opts.dir, "control-plane.json"));
|
|
281781
282895
|
assistantsService.setPlatformAdminChecker(async (caller) => {
|
|
281782
282896
|
const member = await controlPlaneStore.getMember(defaultCompanyId, caller);
|
|
281783
|
-
|
|
281784
|
-
return member.role === "owner";
|
|
282897
|
+
return member?.role === "owner" || member?.role === "admin";
|
|
281785
282898
|
});
|
|
281786
282899
|
if (pgDsn && pgPool) {
|
|
281787
282900
|
const priceStore = await PostgresModelPriceStore.open(pgPool, sharedSchema);
|
|
@@ -281879,16 +282992,29 @@ async function startServe(opts) {
|
|
|
281879
282992
|
}
|
|
281880
282993
|
}
|
|
281881
282994
|
if (!await controlPlaneStore.getMember(defaultCompanyId, operatorActor)) {
|
|
281882
|
-
await controlPlaneStore.
|
|
282995
|
+
const hasOwner = (await controlPlaneStore.listMembers(defaultCompanyId)).some((m2) => m2.role === "owner");
|
|
282996
|
+
await controlPlaneStore.upsertMember({
|
|
282997
|
+
companyId: defaultCompanyId,
|
|
282998
|
+
accountId: operatorActor,
|
|
282999
|
+
actorId: operatorActor,
|
|
283000
|
+
role: hasOwner ? "admin" : "owner",
|
|
283001
|
+
onboardingCompletedAt: seededAt
|
|
283002
|
+
});
|
|
283003
|
+
if (hasOwner) {
|
|
283004
|
+
console.log(`[serve] \u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8FD0\u7EF4\u8005 ${operatorActor} \u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09`);
|
|
283005
|
+
}
|
|
281883
283006
|
}
|
|
281884
283007
|
}
|
|
281885
283008
|
const bootstrapEmail = process.env["OASIS_BOOTSTRAP_OWNER_EMAIL"];
|
|
281886
283009
|
if (bootstrapEmail) {
|
|
281887
|
-
const
|
|
281888
|
-
|
|
283010
|
+
const res = await companies.service.grantOwner(defaultCompanyId, bootstrapEmail);
|
|
283011
|
+
const bootstrapAccount = await controlPlaneStore.getAccountByEmail(bootstrapEmail.trim().toLowerCase());
|
|
283012
|
+
console.log(
|
|
283013
|
+
res.granted ? `[serve] bootstrap owner\uFF1A${bootstrapEmail} \u2192 ${res.membership.accountId}\uFF08\u9ED8\u8BA4\u516C\u53F8 owner\uFF09` : `[serve] bootstrap owner \u8DF3\u8FC7\uFF1A\u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.membership.accountId}\uFF08\u6362\u4EBA\u8D70\u8F6C\u4EA4\uFF09`
|
|
283014
|
+
);
|
|
281889
283015
|
const bootstrapPw = process.env["OASIS_BOOTSTRAP_OWNER_PASSWORD"];
|
|
281890
|
-
if (bootstrapPw) {
|
|
281891
|
-
await controlPlaneStore.setAccountPassword(
|
|
283016
|
+
if (bootstrapPw && bootstrapAccount) {
|
|
283017
|
+
await controlPlaneStore.setAccountPassword(bootstrapAccount.id, hashPassword(bootstrapPw));
|
|
281892
283018
|
console.log(`[serve] bootstrap owner \u5BC6\u7801\u5DF2\u8BBE\uFF08OASIS_BOOTSTRAP_OWNER_PASSWORD\uFF09`);
|
|
281893
283019
|
}
|
|
281894
283020
|
}
|
|
@@ -282060,6 +283186,36 @@ async function startServe(opts) {
|
|
|
282060
283186
|
planeInflight.set(companyId, building);
|
|
282061
283187
|
return building;
|
|
282062
283188
|
};
|
|
283189
|
+
resolveAssistantsCtxImpl = async (companyId) => {
|
|
283190
|
+
const cid = companyId ?? defaultCompanyId;
|
|
283191
|
+
const ctx = await actors.resolveCtx(cid);
|
|
283192
|
+
const engine2 = await getEngine(cid);
|
|
283193
|
+
const primary = Boolean(
|
|
283194
|
+
pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
|
|
283195
|
+
);
|
|
283196
|
+
const plane = primary ? await openCompanyPlane(cid) : void 0;
|
|
283197
|
+
return {
|
|
283198
|
+
service: new AssistantsService({
|
|
283199
|
+
bindStore: plane?.assistants ?? assistantBindStore,
|
|
283200
|
+
prefsStore: plane?.humanPrefs ?? humanPrefsStore,
|
|
283201
|
+
registry: engine2.registry,
|
|
283202
|
+
actors: ctx.service,
|
|
283203
|
+
// 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
|
|
283204
|
+
companyId: () => cid,
|
|
283205
|
+
listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
|
|
283206
|
+
// owner | admin 同档(组织三层身份 ADR D6.4):只认 owner 会让「管理员」失去今天
|
|
283207
|
+
// 「组织管理者」本就有的助理管理能力——那是本次改动造成的能力回归,不是新规则。
|
|
283208
|
+
isPlatformAdmin: async (caller) => {
|
|
283209
|
+
const member = await controlPlaneStore.getMember(cid, caller);
|
|
283210
|
+
return member?.role === "owner" || member?.role === "admin";
|
|
283211
|
+
},
|
|
283212
|
+
// 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
|
|
283213
|
+
// (新模型下它恰好返回一个元素,`ownerAssistantRuntime` 的「取第一个有着落的」语义不变)。
|
|
283214
|
+
listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
|
|
283215
|
+
}),
|
|
283216
|
+
getActor: (id) => engine2.registry.getActor(id)
|
|
283217
|
+
};
|
|
283218
|
+
};
|
|
282063
283219
|
const openCompanyPlaneUncached = async (companyId) => {
|
|
282064
283220
|
if (!pgPool || !locationStore || !cutoverStore) {
|
|
282065
283221
|
throw new Error("openCompanyPlane \u9700\u8981 Postgres");
|
|
@@ -283121,34 +284277,10 @@ async function startServe(opts) {
|
|
|
283121
284277
|
assistantsDomain({
|
|
283122
284278
|
service: assistantsService,
|
|
283123
284279
|
getActor: (id) => registryStore.getActor(id),
|
|
283124
|
-
|
|
283125
|
-
|
|
283126
|
-
|
|
283127
|
-
|
|
283128
|
-
const primary = Boolean(
|
|
283129
|
-
pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
|
|
283130
|
-
);
|
|
283131
|
-
const plane = primary ? await openCompanyPlane(cid) : void 0;
|
|
283132
|
-
return {
|
|
283133
|
-
service: new AssistantsService({
|
|
283134
|
-
bindStore: plane?.assistants ?? assistantBindStore,
|
|
283135
|
-
prefsStore: plane?.humanPrefs ?? humanPrefsStore,
|
|
283136
|
-
registry: engine2.registry,
|
|
283137
|
-
actors: ctx.service,
|
|
283138
|
-
// 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
|
|
283139
|
-
companyId: () => cid,
|
|
283140
|
-
listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
|
|
283141
|
-
isPlatformAdmin: async (caller) => {
|
|
283142
|
-
const member = await controlPlaneStore.getMember(cid, caller);
|
|
283143
|
-
return member?.role === "owner";
|
|
283144
|
-
},
|
|
283145
|
-
// 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
|
|
283146
|
-
// (第一个 owner = 建团队的人;三种 store 都按加入先后返回,见控制面一致性套件)。
|
|
283147
|
-
listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
|
|
283148
|
-
}),
|
|
283149
|
-
getActor: (id) => engine2.registry.getActor(id)
|
|
283150
|
-
};
|
|
283151
|
-
}
|
|
284280
|
+
// 与 actors 域的 agentManageGuard.boundHuman **同一份**:「这个 agent 绑给了哪个真人」
|
|
284281
|
+
// 两处各解析一次助理 store 必然漂,而漂出来的差异正好是「删助理」与「解绑助理」两条路
|
|
284282
|
+
// 的闸又对不上(G4 的形状)。
|
|
284283
|
+
resolveService: resolveAssistantsCtx
|
|
283152
284284
|
}),
|
|
283153
284285
|
companies.register,
|
|
283154
284286
|
projectsDomain2.register,
|
|
@@ -292604,7 +293736,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
|
|
|
292604
293736
|
}
|
|
292605
293737
|
}
|
|
292606
293738
|
}
|
|
292607
|
-
var CLI_VERSION = "2.2.
|
|
293739
|
+
var CLI_VERSION = "2.2.82" ? "2.2.82" : "dev";
|
|
292608
293740
|
function apiHeaders(token, company) {
|
|
292609
293741
|
const headers = {
|
|
292610
293742
|
authorization: `Bearer ${token}`,
|
|
@@ -293001,8 +294133,19 @@ async function runCli(argv, println = console.log, progressln = console.error) {
|
|
|
293001
294133
|
const name = flags.get("name");
|
|
293002
294134
|
if (!await store.getCompany(companyId)) throw new Error(`\u516C\u53F8\u4E0D\u5B58\u5728\uFF1A${companyId}\uFF08\u5148\u8BA9 serve \u8DD1\u4E00\u6B21\u4EE5\u5EFA\u9ED8\u8BA4\u516C\u53F8\uFF0C\u6216\u7528 --company \u6307\u5B9A\u5DF2\u5B58\u5728\u516C\u53F8\uFF09`);
|
|
293003
294135
|
const svc = new CompaniesService({ store });
|
|
293004
|
-
const
|
|
293005
|
-
|
|
294136
|
+
const res = await svc.grantOwner(companyId, email2, name);
|
|
294137
|
+
if (!res.granted && res.existingOwner) {
|
|
294138
|
+
process.stderr.write(
|
|
294139
|
+
`\u516C\u53F8 ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.existingOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${email2}\uFF1B\u4ED6\u73B0\u5728\u662F\u672C\u7EC4\u7EC7\u7684\u300C${res.membership.role === "admin" ? "\u7BA1\u7406\u5458" : res.membership.role}\u300D\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002
|
|
294140
|
+
\u6362\u4EBA\u8BF7\u8BA9\u73B0\u4EFB\u6301\u6709\u4EBA\u8C03 POST /api/companies/${companyId}/owner\uFF08\u8F6C\u4EA4\uFF09\u3002
|
|
294141
|
+
`
|
|
294142
|
+
);
|
|
294143
|
+
process.exitCode = 1;
|
|
294144
|
+
return;
|
|
294145
|
+
}
|
|
294146
|
+
println(
|
|
294147
|
+
res.granted ? `\u5DF2\u6388 owner\uFF1A${email2} \u2192 ${res.membership.accountId}\uFF08\u516C\u53F8 ${companyId}\uFF09` : `${email2} \u5DF2\u662F\u516C\u53F8 ${companyId} \u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u65E0\u9700\u6539\u52A8\uFF08\u5E42\u7B49\uFF09`
|
|
294148
|
+
);
|
|
293006
294149
|
process.stderr.write("\u63D0\u793A\uFF1A\u82E5 serve \u6B63\u5728\u8FD0\u884C\uFF0C\u63A7\u5236\u9762\u6587\u4EF6\u65E0\u9501\uFF0C\u5EFA\u8BAE\u505C\u6B62 serve \u540E\u518D\u6267\u884C\u4EE5\u514D\u5E76\u53D1\u5199\u3002\n");
|
|
293007
294150
|
return;
|
|
293008
294151
|
}
|
|
@@ -294390,7 +295533,13 @@ ${res.warning}`);
|
|
|
294390
295533
|
if (live.length > 0) {
|
|
294391
295534
|
for (const r of live) println(`[design] \u5F15\u64CE\u5728\u8DD1\uFF1Arun ${r.runId.slice(0, 8)} \u72B6\u6001=${r.status}\u2014\u2014\u522B\u518D\u8D77\u65B0\u8F6E\uFF1B\u8FC7\u51E0\u5206\u949F\u518D status\uFF0C\u6216\u672C\u8F6E\u6536\u5DE5\u7B49\u91CD\u6D3E\u3002`);
|
|
294392
295535
|
} else if (st.runs.length > 0) {
|
|
294393
|
-
for (const r of st.runs.slice(0, 3))
|
|
295536
|
+
for (const r of st.runs.slice(0, 3)) {
|
|
295537
|
+
println(`[design] \u8FD1\u671F run\uFF1A${r.runId.slice(0, 8)} \u7EC8\u6001=${r.status}${r.agentId ? ` \u5F15\u64CE=${r.agentId}` : ""}`);
|
|
295538
|
+
if (r.status === "failed" && r.error) {
|
|
295539
|
+
const reason = r.error.length > 300 ? `${r.error.slice(0, 300)}\u2026` : r.error;
|
|
295540
|
+
println(`[design] \u5931\u8D25\u539F\u56E0\uFF08od \u539F\u6587\uFF09\uFF1A${reason}`);
|
|
295541
|
+
}
|
|
295542
|
+
}
|
|
294394
295543
|
} else {
|
|
294395
295544
|
println(`[design] \u65E0\u5728\u4E16 run \u8BB0\u5F55\uFF08od \u91CD\u542F\u540E\u4E3A\u7A7A\uFF0C\u5C5E\u6B63\u5E38\uFF09\u2014\u2014\u770B\u4E0B\u9762\u677F\u4E0A\u4EA7\u7269\u5373\u6301\u4E45\u771F\u76F8\u3002`);
|
|
294396
295545
|
}
|
|
@@ -294402,7 +295551,7 @@ ${res.warning}`);
|
|
|
294402
295551
|
for (const a of st.artifacts) println(` - ${a.path}\uFF08${a.kind ?? "?"}\uFF09status=${a.status ?? "?"}${a.mtime ? ` \xB7 ${new Date(a.mtime).toISOString()}` : ""}`);
|
|
294403
295552
|
if (flags.get("pull") === "true") {
|
|
294404
295553
|
const odPublic = (flags.get("od-public-url") ?? process.env["OASIS_OPEN_DESIGN_PUBLIC_URL"] ?? "https://design.open-friday.com").replace(/\/$/, "");
|
|
294405
|
-
const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, st.runs
|
|
295554
|
+
const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, latestSucceededRun(st.runs)?.runId ?? "status-pull");
|
|
294406
295555
|
println(`[design] \u5DF2\u62C9\u5230\u672C\u5730\uFF1A${dir2}\uFF08\u5165\u53E3 ${entry}\uFF09`);
|
|
294407
295556
|
println(`[design] \u8BFB\u5B8C\u6EE1\u610F\u5373\u4EA4\u7A3F\uFF1Aoasis propose ${artifactId} --from-dir ${dir2} --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"`);
|
|
294408
295557
|
} else {
|
|
@@ -296503,7 +297652,7 @@ function shimScript() {
|
|
|
296503
297652
|
}
|
|
296504
297653
|
|
|
296505
297654
|
// src/index.ts
|
|
296506
|
-
var PKG_VERSION = true ? "2.2.
|
|
297655
|
+
var PKG_VERSION = true ? "2.2.82" : "dev";
|
|
296507
297656
|
var LOCAL_BIN = localBin();
|
|
296508
297657
|
var NPM_PREFIX = npmPrefix();
|
|
296509
297658
|
var INSTANCE = DEFAULT_INSTANCE;
|