oasis_test_v2 0.2.1 → 2.0.0
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 +649 -88
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -820,6 +820,16 @@ var init_parse_extra_args = __esm({
|
|
|
820
820
|
}
|
|
821
821
|
});
|
|
822
822
|
|
|
823
|
+
// ../contract/src/daemon-package.ts
|
|
824
|
+
var DAEMON_NPM_PACKAGE, LEGACY_DAEMON_NPM_PACKAGE;
|
|
825
|
+
var init_daemon_package = __esm({
|
|
826
|
+
"../contract/src/daemon-package.ts"() {
|
|
827
|
+
"use strict";
|
|
828
|
+
DAEMON_NPM_PACKAGE = "oasis_test_v2";
|
|
829
|
+
LEGACY_DAEMON_NPM_PACKAGE = "oasis_test";
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
823
833
|
// ../contract/src/daemon-protocol.ts
|
|
824
834
|
var SESSION_LIMITS;
|
|
825
835
|
var init_daemon_protocol = __esm({
|
|
@@ -1529,6 +1539,7 @@ var init_src = __esm({
|
|
|
1529
1539
|
init_runtime();
|
|
1530
1540
|
init_handle_passthrough();
|
|
1531
1541
|
init_parse_extra_args();
|
|
1542
|
+
init_daemon_package();
|
|
1532
1543
|
init_daemon_protocol();
|
|
1533
1544
|
init_link_windows();
|
|
1534
1545
|
init_registry();
|
|
@@ -3822,6 +3833,11 @@ function resolveNodeReviewers(args) {
|
|
|
3822
3833
|
if (reviewers.some((x2) => x2.reviewerActorId === r.actor && x2.reviewGroup === reviewGroup)) continue;
|
|
3823
3834
|
reviewers.push({ reviewerActorId: r.actor, reviewGroup, source: r.source });
|
|
3824
3835
|
}
|
|
3836
|
+
for (const actor of args.closureGate ?? []) {
|
|
3837
|
+
const group = `closure:${actor}`;
|
|
3838
|
+
if (reviewers.some((x2) => x2.reviewerActorId === actor && x2.reviewGroup === group)) continue;
|
|
3839
|
+
reviewers.push({ reviewerActorId: actor, reviewGroup: group, source: "closure" });
|
|
3840
|
+
}
|
|
3825
3841
|
return { reviewers, staffingIssues };
|
|
3826
3842
|
}
|
|
3827
3843
|
function auditReviewGateStaffing(kernel) {
|
|
@@ -8281,6 +8297,40 @@ var init_state = __esm({
|
|
|
8281
8297
|
}
|
|
8282
8298
|
});
|
|
8283
8299
|
|
|
8300
|
+
// ../engine/src/closure-gate.ts
|
|
8301
|
+
function engineClosureSlot(state) {
|
|
8302
|
+
const live = state.liveNodes();
|
|
8303
|
+
const report = live.find((n) => n.type === DELIVERY_SUMMARY_TYPE);
|
|
8304
|
+
if (report) return report;
|
|
8305
|
+
const business = live.filter((n) => n.type !== BRIEF_TYPE && n.type !== DELIVERY_SUMMARY_TYPE);
|
|
8306
|
+
if (business.length === 1) return business[0];
|
|
8307
|
+
return null;
|
|
8308
|
+
}
|
|
8309
|
+
function engineWorkorderHumanOwner(state) {
|
|
8310
|
+
const brief = state.liveNodes().find((n) => n.type === BRIEF_TYPE);
|
|
8311
|
+
const owner = brief?.assigneeActorId;
|
|
8312
|
+
if (!owner || !owner.startsWith(HUMAN_ACTOR_PREFIX)) return null;
|
|
8313
|
+
return owner;
|
|
8314
|
+
}
|
|
8315
|
+
function engineHumanGateActors(state, nodeId) {
|
|
8316
|
+
const slot = engineClosureSlot(state);
|
|
8317
|
+
if (!slot || slot.id !== nodeId) return [];
|
|
8318
|
+
const owner = engineWorkorderHumanOwner(state);
|
|
8319
|
+
return owner === null ? [] : [owner];
|
|
8320
|
+
}
|
|
8321
|
+
function closureReviewGroup(actor) {
|
|
8322
|
+
return `closure:${actor}`;
|
|
8323
|
+
}
|
|
8324
|
+
var DELIVERY_SUMMARY_TYPE, BRIEF_TYPE, HUMAN_ACTOR_PREFIX;
|
|
8325
|
+
var init_closure_gate2 = __esm({
|
|
8326
|
+
"../engine/src/closure-gate.ts"() {
|
|
8327
|
+
"use strict";
|
|
8328
|
+
DELIVERY_SUMMARY_TYPE = "delivery-summary";
|
|
8329
|
+
BRIEF_TYPE = "brief";
|
|
8330
|
+
HUMAN_ACTOR_PREFIX = "actor:human:";
|
|
8331
|
+
}
|
|
8332
|
+
});
|
|
8333
|
+
|
|
8284
8334
|
// ../engine/src/letters.ts
|
|
8285
8335
|
function repliesSorted(state, issueId) {
|
|
8286
8336
|
return [...state.repliesOf(issueId)].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
|
|
@@ -8475,6 +8525,21 @@ function scan(state, ctx) {
|
|
|
8475
8525
|
const candidate = lsDelivery && ls.conclusion != null && ls.acceptanceState !== "accepted" && ls.acceptanceState !== "rejected" && node2.latestAcceptId !== ls.id ? ls : null;
|
|
8476
8526
|
const rejectedDelivery = lsDelivery && ls.conclusion != null && ls.acceptanceState === "rejected" && node2.latestAcceptId !== ls.id ? ls : null;
|
|
8477
8527
|
if (candidate) {
|
|
8528
|
+
const closureActors = engineHumanGateActors(state, node2.id);
|
|
8529
|
+
if (closureActors.length > 0 && needsClosureRequirementUpdate(state, node2.id, closureActors)) {
|
|
8530
|
+
events.push({
|
|
8531
|
+
kind: "plan.update_review_requirements",
|
|
8532
|
+
workorderId: state.workorder.id,
|
|
8533
|
+
nodeId: node2.id,
|
|
8534
|
+
// ★ 合并写:保留 source ∈ {type, manual, model} 的原有行,覆写 source='closure' 的行。
|
|
8535
|
+
// `plan.update_review_requirements` handler 是**全量替换**(killNodeReviews + replaceRequirements),
|
|
8536
|
+
// 所以必须把手动 / 类型 / 模型来源的行一并带上——漏一条即误删协调者手动指派的审核人。
|
|
8537
|
+
// killNodeReviews 会杀本节点在跑的评审:candidate 分支进入时该节点尚未派出评审(reqs 为空
|
|
8538
|
+
// or 只含 closure 之外的组),本轮内 kill 无影响。
|
|
8539
|
+
reviewers: mergedClosureRequirements(state, node2.id, closureActors)
|
|
8540
|
+
});
|
|
8541
|
+
continue;
|
|
8542
|
+
}
|
|
8478
8543
|
const reqs = state.requirementsOf(node2.id);
|
|
8479
8544
|
if (reqs.length === 0) {
|
|
8480
8545
|
if (acceptBlock(state, candidate.id) !== null) continue;
|
|
@@ -9013,6 +9078,21 @@ function checkTerminated(state) {
|
|
|
9013
9078
|
}
|
|
9014
9079
|
return live.every((n) => n.latestAcceptId !== null && (n.latestAcceptId === n.latestWorkId || n.latestAcceptId === n.latestProposedWorkId));
|
|
9015
9080
|
}
|
|
9081
|
+
function needsClosureRequirementUpdate(state, nodeId, closureActors) {
|
|
9082
|
+
const existing = state.requirementsOf(nodeId).filter((r) => r.source === "closure");
|
|
9083
|
+
const have = new Set(existing.map((r) => r.reviewerActorId));
|
|
9084
|
+
if (have.size !== closureActors.length) return true;
|
|
9085
|
+
return !closureActors.every((a) => have.has(a));
|
|
9086
|
+
}
|
|
9087
|
+
function mergedClosureRequirements(state, nodeId, closureActors) {
|
|
9088
|
+
const kept = state.requirementsOf(nodeId).filter((r) => r.source !== "closure").map((r) => ({ reviewerActorId: r.reviewerActorId, reviewGroup: r.reviewGroup, source: r.source }));
|
|
9089
|
+
const closureRows = closureActors.map((actor) => ({
|
|
9090
|
+
reviewerActorId: actor,
|
|
9091
|
+
reviewGroup: closureReviewGroup(actor),
|
|
9092
|
+
source: "closure"
|
|
9093
|
+
}));
|
|
9094
|
+
return [...kept, ...closureRows];
|
|
9095
|
+
}
|
|
9016
9096
|
function isEdgeInputUnobtainable(state, edge) {
|
|
9017
9097
|
const upstream = state.node(edge.fromNodeId);
|
|
9018
9098
|
return !!upstream?.cancelledAt && !upstream.latestAcceptId;
|
|
@@ -9126,6 +9206,7 @@ var init_activate = __esm({
|
|
|
9126
9206
|
init_src3();
|
|
9127
9207
|
init_src2();
|
|
9128
9208
|
init_model();
|
|
9209
|
+
init_closure_gate2();
|
|
9129
9210
|
init_letters();
|
|
9130
9211
|
init_model();
|
|
9131
9212
|
init_views2();
|
|
@@ -9202,7 +9283,9 @@ CREATE TABLE IF NOT EXISTS ${q2}.review_requirements (
|
|
|
9202
9283
|
node_id text NOT NULL,
|
|
9203
9284
|
reviewer_actor_id text NOT NULL,
|
|
9204
9285
|
review_group text NOT NULL,
|
|
9205
|
-
|
|
9286
|
+
-- \u2605 'closure' \u7531 scan \u5728\u7ED3\u6848\u4F4D\u6536\u53E3\u90A3\u4E00\u523B\u5199\u5165\uFF08{@link ../../core/src/closure-gate.ts}\uFF09\uFF0C\u662F**\u7CFB\u7EDF\u6D3E\u751F**\u7684
|
|
9287
|
+
-- \u7ED3\u6848\u4EBA\u95F8\uFF1B\u4E0E manual \u5206\u5BB6\uFF0C\u91CD\u7B97\u65F6\u53EA\u8986\u76D6 source='closure' \u7684\u884C\uFF0C\u4E00\u6761 manual \u90FD\u4E0D\u52A8\u3002
|
|
9288
|
+
source text NOT NULL CHECK (source IN ('type','manual','model','closure')),
|
|
9206
9289
|
assigned_by text NOT NULL,
|
|
9207
9290
|
assigned_at timestamptz NOT NULL,
|
|
9208
9291
|
latest_review_id text,
|
|
@@ -9412,6 +9495,11 @@ CREATE TABLE IF NOT EXISTS ${q2}.event_handler_log (
|
|
|
9412
9495
|
);
|
|
9413
9496
|
|
|
9414
9497
|
-- \u2500\u2500 \u8FC1\u79FB\uFF1A\u65E2\u6709\u90E8\u7F72\u7684\u5217\u589E\u91CF\uFF08CREATE TABLE IF NOT EXISTS \u4E0D\u52A0\u5217\uFF0C\u8FD9\u91CC\u8865\uFF09\u2500\u2500
|
|
9498
|
+
-- \u2605 review_requirements.source \u6269\u4E3A\u56DB\u503C\uFF08+'closure'\uFF09\uFF1A\u7ED3\u6848\u4EBA\u95F8\u7531 scan \u5728\u6536\u53E3\u90A3\u4E00\u523B\u5199\u5165\uFF0C\u4E0E manual \u5206\u5BB6\u3002
|
|
9499
|
+
-- \u5199\u6CD5\u4E0E\u4E0B\u65B9 works_lane_check \u540C\u6B3E\uFF08\u5148\u5220\u540E\u52A0\uFF0C\u6210\u5BF9\u5E42\u7B49\uFF09\u3002
|
|
9500
|
+
ALTER TABLE ${q2}.review_requirements DROP CONSTRAINT IF EXISTS review_requirements_source_check;
|
|
9501
|
+
ALTER TABLE ${q2}.review_requirements ADD CONSTRAINT review_requirements_source_check
|
|
9502
|
+
CHECK (source IN ('type','manual','model','closure'));
|
|
9415
9503
|
ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
|
|
9416
9504
|
ALTER TABLE ${q2}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
|
|
9417
9505
|
ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS status text;
|
|
@@ -9512,10 +9600,9 @@ ALTER TABLE ${q2}.workflow_nodes ADD COLUMN IF NOT EXISTS latest_proposed_work_i
|
|
|
9512
9600
|
DROP VIEW IF EXISTS ${q2}.work_view;
|
|
9513
9601
|
DROP VIEW IF EXISTS ${q2}.review_view;
|
|
9514
9602
|
DROP VIEW IF EXISTS ${q2}.node_view;
|
|
9515
|
-
DROP VIEW IF EXISTS ${q2}.workorder_view;
|
|
9516
9603
|
-- ADR 0308\uFF1A\u5220\u9664\u672A\u88AB\u4E1A\u52A1\u8BFB\u53D6\u7684\u5DE5\u5355\u5143\u6570\u636E\u526F\u672C\u3002\u771F\u5B9E\u4FE1\u606F\u4F4D\u4E8E\u8282\u70B9\u6216\u9879\u76EE\u7ED1\u5B9A\u3002
|
|
9517
|
-
-- \
|
|
9518
|
-
-- \
|
|
9604
|
+
-- ADR 0512\uFF1A\u6E05\u7406\u4E0D\u963B\u65AD\u5B58\u91CF\u5E93\u542F\u52A8\u3002\u975E\u7A7A\u65E7\u503C\u6216\u5916\u90E8\u4F9D\u8D56\u5B58\u5728\u65F6\u4FDD\u7559\u6574\u7EC4\u65E7\u5217\u4E0E\u65E7\u89C6\u56FE\uFF0C
|
|
9605
|
+
-- \u7B49\u663E\u5F0F\u6838\u5BF9\u5F52\u6863\u540E\u518D\u6E05\u7406\uFF1B\u4E0D\u731C\u6D4B\u771F\u5B9E\u6765\u6E90\uFF0C\u4E0D\u6E05\u7A7A\u5386\u53F2\u503C\uFF0C\u4E0D\u7528 CASCADE\u3002
|
|
9519
9606
|
DO $$
|
|
9520
9607
|
DECLARE
|
|
9521
9608
|
column_name_to_drop text;
|
|
@@ -9534,18 +9621,25 @@ BEGIN
|
|
|
9534
9621
|
CASE WHEN column_name_to_drop = 'acceptance_criteria' THEN '[]' ELSE '' END
|
|
9535
9622
|
) INTO has_data;
|
|
9536
9623
|
IF has_data THEN
|
|
9537
|
-
RAISE
|
|
9624
|
+
RAISE WARNING 'Retaining legacy workorder metadata in schema %: column % contains data; reconcile and archive before cleanup (ADR 0512)', ${lit}, column_name_to_drop;
|
|
9625
|
+
RETURN;
|
|
9538
9626
|
END IF;
|
|
9539
9627
|
END IF;
|
|
9540
9628
|
END LOOP;
|
|
9629
|
+
BEGIN
|
|
9630
|
+
DROP VIEW IF EXISTS ${q2}.workorder_view;
|
|
9631
|
+
ALTER TABLE ${q2}.workorders
|
|
9632
|
+
DROP COLUMN IF EXISTS project_id,
|
|
9633
|
+
DROP COLUMN IF EXISTS description,
|
|
9634
|
+
DROP COLUMN IF EXISTS goal,
|
|
9635
|
+
DROP COLUMN IF EXISTS acceptance_criteria,
|
|
9636
|
+
DROP COLUMN IF EXISTS manager_actor_id,
|
|
9637
|
+
DROP COLUMN IF EXISTS root_node_id;
|
|
9638
|
+
EXCEPTION WHEN dependent_objects_still_exist THEN
|
|
9639
|
+
-- \u5B50\u4E8B\u52A1\u6062\u590D\u521A\u624D\u5220\u9664\u7684\u81EA\u6709\u89C6\u56FE\u4E0E\u5217\uFF1B\u5916\u90E8\u6D88\u8D39\u8005\u4E0D\u4F1A\u88AB\u6E05\u7406\u987A\u5E26\u5220\u9664\u3002
|
|
9640
|
+
RAISE WARNING 'Retaining legacy workorder metadata in schema %: external dependencies require reconciliation (ADR 0512)', ${lit};
|
|
9641
|
+
END;
|
|
9541
9642
|
END $$;
|
|
9542
|
-
ALTER TABLE ${q2}.workorders
|
|
9543
|
-
DROP COLUMN IF EXISTS project_id,
|
|
9544
|
-
DROP COLUMN IF EXISTS description,
|
|
9545
|
-
DROP COLUMN IF EXISTS goal,
|
|
9546
|
-
DROP COLUMN IF EXISTS acceptance_criteria,
|
|
9547
|
-
DROP COLUMN IF EXISTS manager_actor_id,
|
|
9548
|
-
DROP COLUMN IF EXISTS root_node_id;
|
|
9549
9643
|
|
|
9550
9644
|
-- \u8C03\u5EA6\u5668\u5B8C\u7ED3\u6807\u8BB0\uFF08mark_done/unmark_done \u843D\u70B9\uFF0C2026-08-12\uFF09\u3002
|
|
9551
9645
|
ALTER TABLE ${q2}.workorders ADD COLUMN IF NOT EXISTS done_at timestamptz;
|
|
@@ -9644,10 +9738,9 @@ LEFT JOIN ${q2}.works ls ON ls.id = n.latest_proposed_work_id;
|
|
|
9644
9738
|
-- workorder_view \u5DF2\u4E0B\u7EBF\uFF08\u5DE5\u5355 1aa1f35b\uFF09\uFF1A\u5B83\u53EA\u989D\u5916\u7ED9\u51FA total_count / done_count \u4E24\u4E2A\u8FDB\u5EA6\u6570\uFF0C\u5168\u4ED3\u96F6\u6D88\u8D39\u65B9\uFF0C
|
|
9645
9739
|
-- \u4E14\u53E3\u5F84\u4E0E\u540E\u7AEF\u771F\u6E90\uFF08server summaryOfWorkspace \u7684 progressExcludingBrief\uFF09\u4E0D\u540C\u2014\u2014total \u53EA\u6392 cancelled\u3001done \u7528
|
|
9646
9740
|
-- latest_accept_id IS NOT NULL\uFF08\u66FE\u88AB\u9A8C\u6536\u8FC7\u81F3\u5C11\u4E00\u6B21\uFF09\uFF0C\u800C\u771F\u6E90\u6392\u9664 brief + \u5DF2\u4F5C\u5E9F\u3001\u6309\u300C\u9A8C\u6536\u843D\u5728\u5F53\u524D\u7248\u300D\u7B97\u3002
|
|
9647
|
-
-- \
|
|
9648
|
-
-- \u5728\u4E0B\u6B21 migrate \u65F6\u628A\u65E7\u89C6\u56FE\u5220\u6389\uFF08ENGINE_DDL \u6BB5\u4EA6\u6709\u4E00\u5904 DROP\uFF0C\u4E8C\u8005\u90FD\u7559\u4EE5\u9632\u91CD\u6392\uFF09\u3002\u5DE5\u5355\u8FDB\u5EA6\u7EDF\u4E00\u8D70\u540E\u7AEF\u8BFB\u6A21\u578B\uFF0C
|
|
9741
|
+
-- \u8FD9\u91CC\u4E0D\u518D CREATE\uFF1B\u65E7\u89C6\u56FE\u4EC5\u5728\u5386\u53F2\u5143\u6570\u636E\u9700\u5F52\u6863\u65F6\u6682\u5B58\uFF0C\u4E0D\u53C2\u4E0E\u4E1A\u52A1\u8BFB\u53D6\u3002\u5DE5\u5355\u8FDB\u5EA6\u7EDF\u4E00\u8D70\u540E\u7AEF\u8BFB\u6A21\u578B\uFF0C
|
|
9649
9742
|
-- \u9700\u8981 SQL \u4FA7\u7EDF\u8BA1\u65F6\u53E6\u7ACB\u4E0E\u771F\u6E90\u540C\u53E3\u5F84\u7684\u89C6\u56FE\u5E76\u6CE8\u660E\u670D\u52A1\u5BF9\u8C61\uFF0C\u522B\u590D\u6D3B\u8FD9\u4E24\u4E2A\u6570\u3002
|
|
9650
|
-
|
|
9743
|
+
-- \u6E05\u7406\u7531 ENGINE_DDL \u7684\u5143\u6570\u636E\u6E05\u7406\u5757\u8D1F\u8D23\uFF1B\u4FDD\u7559\u5386\u53F2\u5217\u65F6\u4E5F\u4FDD\u7559\u65E7\u89C6\u56FE\uFF08ADR 0512\uFF09\u3002
|
|
9651
9744
|
`;
|
|
9652
9745
|
}
|
|
9653
9746
|
var init_schema2 = __esm({
|
|
@@ -10823,7 +10916,7 @@ var require_utils2 = __commonJS({
|
|
|
10823
10916
|
var nodeCrypto = require("crypto");
|
|
10824
10917
|
module2.exports = {
|
|
10825
10918
|
postgresMd5PasswordHash,
|
|
10826
|
-
randomBytes:
|
|
10919
|
+
randomBytes: randomBytes13,
|
|
10827
10920
|
deriveKey,
|
|
10828
10921
|
sha256: sha2563,
|
|
10829
10922
|
hashByName,
|
|
@@ -10833,7 +10926,7 @@ var require_utils2 = __commonJS({
|
|
|
10833
10926
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
10834
10927
|
var subtleCrypto = webCrypto.subtle;
|
|
10835
10928
|
var textEncoder = new TextEncoder();
|
|
10836
|
-
function
|
|
10929
|
+
function randomBytes13(length) {
|
|
10837
10930
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
10838
10931
|
}
|
|
10839
10932
|
async function md5(string7) {
|
|
@@ -17487,6 +17580,7 @@ var init_src4 = __esm({
|
|
|
17487
17580
|
init_views2();
|
|
17488
17581
|
init_state();
|
|
17489
17582
|
init_activate();
|
|
17583
|
+
init_closure_gate2();
|
|
17490
17584
|
init_schema2();
|
|
17491
17585
|
init_store();
|
|
17492
17586
|
init_store_memory();
|
|
@@ -17583,7 +17677,11 @@ function nodeToArtifact(node2, edges, requirements) {
|
|
|
17583
17677
|
required: e.required
|
|
17584
17678
|
}));
|
|
17585
17679
|
const nodeReqs = requirements.filter((r) => r.nodeId === node2.id);
|
|
17586
|
-
const reviewers = nodeReqs.length > 0 ? nodeReqs.map((r) => ({
|
|
17680
|
+
const reviewers = nodeReqs.length > 0 ? nodeReqs.map((r) => ({
|
|
17681
|
+
actor: r.reviewerActorId,
|
|
17682
|
+
source: r.source,
|
|
17683
|
+
reviewGroup: r.reviewGroup
|
|
17684
|
+
})) : void 0;
|
|
17587
17685
|
const nodeFields = node2.fields;
|
|
17588
17686
|
const reviewRoles = nodeFields && "reviewRoles" in nodeFields ? nodeFields.reviewRoles : void 0;
|
|
17589
17687
|
const docType = nodeFields && "docType" in nodeFields ? nodeFields.docType : void 0;
|
|
@@ -18701,6 +18799,27 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
18701
18799
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
18702
18800
|
return out;
|
|
18703
18801
|
}
|
|
18802
|
+
function groupRequirementsForPreview(rows) {
|
|
18803
|
+
const groups = /* @__PURE__ */ new Map();
|
|
18804
|
+
for (const r of rows) {
|
|
18805
|
+
const key = r.reviewGroup ?? (r.source === "manual" || r.source === "closure" ? r.actor : "default");
|
|
18806
|
+
const bucket = groups.get(key);
|
|
18807
|
+
if (bucket) {
|
|
18808
|
+
if (!bucket.eligible.includes(r.actor)) bucket.eligible.push(r.actor);
|
|
18809
|
+
} else {
|
|
18810
|
+
groups.set(key, { source: r.source, eligible: [r.actor] });
|
|
18811
|
+
}
|
|
18812
|
+
}
|
|
18813
|
+
const out = [];
|
|
18814
|
+
for (const [key, b2] of groups) {
|
|
18815
|
+
if (b2.source === "type") {
|
|
18816
|
+
out.push({ source: "type", eligible: [], quorum: 1, roles: [key] });
|
|
18817
|
+
} else {
|
|
18818
|
+
out.push({ source: b2.source, eligible: b2.eligible, quorum: 1 });
|
|
18819
|
+
}
|
|
18820
|
+
}
|
|
18821
|
+
return out;
|
|
18822
|
+
}
|
|
18704
18823
|
var import_node_crypto3, PENDING_ROLE_PREFIX2, PENDING_UNASSIGNED2, EngineKernel;
|
|
18705
18824
|
var init_kernel_bridge = __esm({
|
|
18706
18825
|
"../core/src/kernel-bridge.ts"() {
|
|
@@ -19125,26 +19244,50 @@ var init_kernel_bridge = __esm({
|
|
|
19125
19244
|
reviewerPreview(artifactId) {
|
|
19126
19245
|
const a = this.model.artifacts.get(artifactId);
|
|
19127
19246
|
if (!a) return null;
|
|
19128
|
-
const
|
|
19129
|
-
|
|
19130
|
-
|
|
19131
|
-
|
|
19247
|
+
const nodeReqs = a.reviewers ?? [];
|
|
19248
|
+
if (nodeReqs.length > 0) {
|
|
19249
|
+
const requirements2 = groupRequirementsForPreview(nodeReqs);
|
|
19250
|
+
const manualLikeActors2 = nodeReqs.filter((r) => r.source === "manual" || r.source === "closure").map((r) => ({ actor: r.actor, source: r.source }));
|
|
19251
|
+
return {
|
|
19252
|
+
reviewers: manualLikeActors2,
|
|
19253
|
+
quorum: requirements2.length,
|
|
19254
|
+
requirements: requirements2,
|
|
19255
|
+
typeName: a.type
|
|
19256
|
+
};
|
|
19132
19257
|
}
|
|
19133
19258
|
const typeDef = this.schema.get(a.type);
|
|
19134
|
-
const
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
}
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19259
|
+
const closureGate = humanGateActorsOf(this.model, artifactId);
|
|
19260
|
+
const { reviewers: predicted } = resolveNodeReviewers({
|
|
19261
|
+
typeName: a.type,
|
|
19262
|
+
typeDef,
|
|
19263
|
+
...a.reviewRoles !== void 0 ? { reviewRoles: a.reviewRoles } : {},
|
|
19264
|
+
...a.reviewers !== void 0 && a.reviewers.length > 0 ? { explicitReviewers: a.reviewers.map((r) => ({
|
|
19265
|
+
actor: r.actor,
|
|
19266
|
+
source: r.source === "type" || r.source === "closure" ? "manual" : r.source
|
|
19267
|
+
})) } : {},
|
|
19268
|
+
roleIndex: this.roleIndex,
|
|
19269
|
+
...closureGate.length > 0 ? { closureGate } : {}
|
|
19270
|
+
});
|
|
19271
|
+
if (predicted.length === 0) {
|
|
19272
|
+
return {
|
|
19273
|
+
reviewers: [],
|
|
19274
|
+
quorum: 0,
|
|
19275
|
+
requirements: [],
|
|
19276
|
+
typeName: a.type
|
|
19277
|
+
};
|
|
19143
19278
|
}
|
|
19279
|
+
const shape = predicted.map((r) => ({
|
|
19280
|
+
actor: r.reviewerActorId,
|
|
19281
|
+
source: r.source,
|
|
19282
|
+
reviewGroup: r.reviewGroup
|
|
19283
|
+
}));
|
|
19284
|
+
const requirements = groupRequirementsForPreview(shape);
|
|
19285
|
+
const manualLikeActors = shape.filter((r) => r.source === "manual" || r.source === "closure").map((r) => ({ actor: r.actor, source: r.source }));
|
|
19144
19286
|
return {
|
|
19145
|
-
reviewers:
|
|
19146
|
-
quorum:
|
|
19147
|
-
requirements
|
|
19287
|
+
reviewers: manualLikeActors,
|
|
19288
|
+
quorum: requirements.length,
|
|
19289
|
+
requirements,
|
|
19290
|
+
predicted: true,
|
|
19148
19291
|
typeName: a.type
|
|
19149
19292
|
};
|
|
19150
19293
|
}
|
|
@@ -42410,7 +42553,7 @@ var require_websocket = __commonJS({
|
|
|
42410
42553
|
var http2 = require("http");
|
|
42411
42554
|
var net = require("net");
|
|
42412
42555
|
var tls = require("tls");
|
|
42413
|
-
var { randomBytes:
|
|
42556
|
+
var { randomBytes: randomBytes13, createHash: createHash37 } = require("crypto");
|
|
42414
42557
|
var { Duplex, Readable } = require("stream");
|
|
42415
42558
|
var { URL: URL2 } = require("url");
|
|
42416
42559
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -42948,7 +43091,7 @@ var require_websocket = __commonJS({
|
|
|
42948
43091
|
}
|
|
42949
43092
|
}
|
|
42950
43093
|
const defaultPort = isSecure ? 443 : 80;
|
|
42951
|
-
const key =
|
|
43094
|
+
const key = randomBytes13(16).toString("base64");
|
|
42952
43095
|
const request2 = isSecure ? https.request : http2.request;
|
|
42953
43096
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
42954
43097
|
let perMessageDeflate;
|
|
@@ -153545,11 +153688,14 @@ function planningStatusOf(_model, arts, resolveRoleExecutor) {
|
|
|
153545
153688
|
for (const a of arts) {
|
|
153546
153689
|
if (!isPendingOwner(a.owner)) continue;
|
|
153547
153690
|
if (a.owner === PENDING_UNASSIGNED) {
|
|
153691
|
+
const label = nodeLabel(a);
|
|
153548
153692
|
issues.push({
|
|
153549
153693
|
code: "missing_owner",
|
|
153550
153694
|
severity: "warning",
|
|
153551
153695
|
artifactType: a.type,
|
|
153552
|
-
|
|
153696
|
+
artifactId: a.id,
|
|
153697
|
+
label,
|
|
153698
|
+
message: `\u300C${label}\u300D\u7684\u7C7B\u578B\u4E0D\u7ED1\u5B9A\u5C97\u4F4D\uFF08\u8C01\u90FD\u80FD\u5199\uFF09\uFF0C\u5C1A\u672A\u6307\u5B9A\u8D1F\u8D23\u4EBA\u2014\u2014\u8BF7 assign\u3002`
|
|
153553
153699
|
});
|
|
153554
153700
|
continue;
|
|
153555
153701
|
}
|
|
@@ -153569,24 +153715,33 @@ function planningStatusOf(_model, arts, resolveRoleExecutor) {
|
|
|
153569
153715
|
code: "missing_role",
|
|
153570
153716
|
severity: "warning",
|
|
153571
153717
|
role: p2.role,
|
|
153572
|
-
message: `${p2.role} \u89D2\u8272\u65E0 active \u5458\u5DE5\uFF0C${a
|
|
153718
|
+
message: `${p2.role} \u89D2\u8272\u65E0 active \u5458\u5DE5\uFF0C\u300C${nodeLabel(a)}\u300D\u7684\u90E8\u4EF6 ${p2.name} \u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216\u6362 role\u3002`
|
|
153573
153719
|
});
|
|
153574
153720
|
}
|
|
153575
153721
|
}
|
|
153576
153722
|
}
|
|
153577
153723
|
return issues.length > 0 ? { status: "needs_staffing", issues } : { status: "ready", issues: [] };
|
|
153578
153724
|
}
|
|
153725
|
+
function draftNodeLabel(id, type, title) {
|
|
153726
|
+
if (title) return title;
|
|
153727
|
+
if (!id) return `\u672A\u547D\u540D ${type}`;
|
|
153728
|
+
const tail = id.split(":").slice(2).join(":");
|
|
153729
|
+
return tail ? `\u672A\u547D\u540D ${type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${type}`;
|
|
153730
|
+
}
|
|
153579
153731
|
function draftPlanningStatusOf(spawns, isRoleStaffed) {
|
|
153580
153732
|
const issues = [];
|
|
153581
153733
|
const seenRoles = /* @__PURE__ */ new Set();
|
|
153582
153734
|
for (const s2 of spawns) {
|
|
153583
153735
|
const owner = s2.owner;
|
|
153584
153736
|
if (owner === void 0 || owner === PENDING_UNASSIGNED) {
|
|
153737
|
+
const label = draftNodeLabel(s2.id, s2.type, s2.title);
|
|
153585
153738
|
issues.push({
|
|
153586
153739
|
code: "missing_owner",
|
|
153587
153740
|
severity: "warning",
|
|
153588
153741
|
artifactType: s2.type,
|
|
153589
|
-
|
|
153742
|
+
...s2.id ? { artifactId: s2.id } : {},
|
|
153743
|
+
label,
|
|
153744
|
+
message: `\u300C${label}\u300D\u5C1A\u672A\u6307\u5B9A\u8D1F\u8D23\u4EBA\u2014\u2014\u5EFA\u5355\u524D\u8BF7\u5728\u8349\u6848\u91CC\u6307\u5B9A\uFF0C\u5426\u5219\u843D\u6210\u5F85\u6D3E\u3002`
|
|
153590
153745
|
});
|
|
153591
153746
|
continue;
|
|
153592
153747
|
}
|
|
@@ -153814,6 +153969,8 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
|
|
|
153814
153969
|
}),
|
|
153815
153970
|
// 0101:审核角色是否被覆盖过(替换了类型默认)——供详情页角色位徽标/复原判断。
|
|
153816
153971
|
...a.reviewRoles !== void 0 ? { reviewRolesOverridden: true } : {},
|
|
153972
|
+
// 预计标:结案位在报告交付前展示时会带;落库后消失。前端据此区分「预计要谁审」vs「已确定要谁审」。
|
|
153973
|
+
...pv.predicted ? { reviewersPredicted: true } : {},
|
|
153817
153974
|
typeName: pv.typeName
|
|
153818
153975
|
} : {},
|
|
153819
153976
|
// 过期事实(提案 rework-cascade-rollout):无条件透出,前端 DAG 挂「上游已更新」徽标。
|
|
@@ -153978,6 +154135,9 @@ function buildWorkorderDraftDetail(draft, resolveActor, typeReviewerPreview, res
|
|
|
153978
154135
|
type: op.type,
|
|
153979
154136
|
label: op.title ?? op.description ?? op.id,
|
|
153980
154137
|
stage: "embryo",
|
|
154138
|
+
// 草案节点从没跑过——补 state:"not_run"(与真工单节点 `evaluateNodeRunState` 同一档),
|
|
154139
|
+
// 否则 WorkorderNodeCard 的 `state ? NODE_STATE_LABEL[state] : "已废弃"` 会给每张卡印上「已废弃」。
|
|
154140
|
+
state: "not_run",
|
|
153981
154141
|
currentRev: null,
|
|
153982
154142
|
queue: 0,
|
|
153983
154143
|
blocked: false,
|
|
@@ -154014,7 +154174,15 @@ function buildWorkorderDraftDetail(draft, resolveActor, typeReviewerPreview, res
|
|
|
154014
154174
|
...draft.seed.dispatch === false ? { dispatchPaused: true } : {}
|
|
154015
154175
|
};
|
|
154016
154176
|
const planning = draftPlanningStatusOf(
|
|
154017
|
-
spawns.filter((op) => op.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE).map((op) =>
|
|
154177
|
+
spawns.filter((op) => op.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE).map((op) => {
|
|
154178
|
+
const title2 = op.title ?? op.description;
|
|
154179
|
+
return {
|
|
154180
|
+
...op.id ? { id: op.id } : {},
|
|
154181
|
+
type: op.type,
|
|
154182
|
+
...op.owner !== void 0 ? { owner: op.owner } : {},
|
|
154183
|
+
...title2 !== void 0 ? { title: title2 } : {}
|
|
154184
|
+
};
|
|
154185
|
+
}),
|
|
154018
154186
|
resolveRoleStaffed ?? (() => true)
|
|
154019
154187
|
);
|
|
154020
154188
|
return {
|
|
@@ -162398,7 +162566,7 @@ async function deliverWorkorderBroadcast(deps, terminal) {
|
|
|
162398
162566
|
const isEnqueueFailed = reason.includes(";enqueue-failed:");
|
|
162399
162567
|
const isNotWired = reason.includes(";enqueue-not-wired");
|
|
162400
162568
|
const isSkipped = reason.includes(";enqueue-skipped");
|
|
162401
|
-
const classification = isEnqueueFailed ? "\u5B58\u50A8\u5C42\u6545\u969C\uFF08\u53D1\u8D77\u4EBA\u5DF2\u63A5\u53D7\u7684\u6B8B\u4F59\u98CE\u9669\uFF1A\u5165\u961F\u771F\u53BB\u4E86\u5374\u6296\u5230\u5E95\uFF0C\u804A\u5929\u672C\u8EAB\u4E5F\u5199\u4E0D\u8FDB\u53BB\uFF09" : isNotWired ? "\
|
|
162569
|
+
const classification = isEnqueueFailed ? "\u5B58\u50A8\u5C42\u6545\u969C\uFF08\u53D1\u8D77\u4EBA\u5DF2\u63A5\u53D7\u7684\u6B8B\u4F59\u98CE\u9669\uFF1A\u5165\u961F\u771F\u53BB\u4E86\u5374\u6296\u5230\u5E95\uFF0C\u804A\u5929\u672C\u8EAB\u4E5F\u5199\u4E0D\u8FDB\u53BB\uFF09" : isNotWired ? "\u672A\u88C5\u914D enqueuePending\uFF0C\u751F\u4EA7\u5FC5\u987B\u63A5\u961F\u5217\uFF08\u6700\u5C0F\u88C5\u914D / \u5355\u6D4B\u624D\u5141\u8BB8\u7F3A\u88C5\u914D\uFF09" : isSkipped ? "flush \u89E6\u53D1\u5668\u4F20\u5165 skipEnqueueOnFail=true\uFF0C\u672C\u884C\u4ECD\u5728\u961F\u5217\u91CC\uFF0C\u4F1A\u7531\u4E0B\u4E00\u573A flush \u8BA4\u9886" : "\u9996\u6B21\u6295\u9012\u5931\u8D25\u4E14\u672A\u8D70\u5165\u961F\uFF08\u672A\u9884\u671F\u5F52\u56E0\uFF0C\u8BF7\u6838\u67E5 deliverToAgent \u662F\u5426\u6F0F\u8D70 enqueueOrFail\uFF09";
|
|
162402
162570
|
log3(
|
|
162403
162571
|
`[chat-broadcast] \u26A0 \u901A\u544A\u5DF2\u843D\u5E93\u3001\u6295\u9012\u672A\u6210\u529F\uFF1A\u515C\u5E95\u634E\u5E26\u770B\u4E0D\u5230 delivery_route='queue' \u7684\u884C\uFF0C \u552F\u4E00\u9001\u53D1\u8005\u662F flusher\u3002 wo=${terminal.workOrderId} session=${originId} messageId=${messageId} reason=${reason} \u2014\u2014 ${classification}`
|
|
162404
162572
|
);
|
|
@@ -162817,6 +162985,10 @@ function invitationJoinUrl(consoleUrl, invitationId) {
|
|
|
162817
162985
|
if (!consoleUrl) return void 0;
|
|
162818
162986
|
return joinConsoleUrl(consoleUrl, `/invite/${encodeURIComponent(invitationId)}`);
|
|
162819
162987
|
}
|
|
162988
|
+
function inviteLinkJoinUrl(consoleUrl, token) {
|
|
162989
|
+
if (!consoleUrl) return void 0;
|
|
162990
|
+
return joinConsoleUrl(consoleUrl, `/invite/${encodeURIComponent(token)}`);
|
|
162991
|
+
}
|
|
162820
162992
|
var init_console_url = __esm({
|
|
162821
162993
|
"../server/src/domains/shared/console-url.ts"() {
|
|
162822
162994
|
"use strict";
|
|
@@ -202866,6 +203038,31 @@ async function startOasisServer(opts) {
|
|
|
202866
203038
|
return;
|
|
202867
203039
|
}
|
|
202868
203040
|
}
|
|
203041
|
+
const staffedRoles = /* @__PURE__ */ new Set();
|
|
203042
|
+
if (principalRegistry) {
|
|
203043
|
+
const actorsForStaffing = await principalRegistry.listActors();
|
|
203044
|
+
for (const a of actorsForStaffing) if (a.status === "active") for (const r of a.roles) staffedRoles.add(r);
|
|
203045
|
+
}
|
|
203046
|
+
const spawnedForPlanning = body2.plan.ops.filter((op) => op.action === "spawn").filter((op) => op.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE).map((op) => {
|
|
203047
|
+
const title = op.title ?? op.description;
|
|
203048
|
+
return {
|
|
203049
|
+
...op.id ? { id: op.id } : {},
|
|
203050
|
+
type: op.type,
|
|
203051
|
+
...op.owner !== void 0 ? { owner: op.owner } : {},
|
|
203052
|
+
...title !== void 0 ? { title } : {}
|
|
203053
|
+
};
|
|
203054
|
+
});
|
|
203055
|
+
const planning = draftPlanningStatusOf(
|
|
203056
|
+
spawnedForPlanning,
|
|
203057
|
+
(role) => staffedRoles.has(role)
|
|
203058
|
+
);
|
|
203059
|
+
if (planning.status === "needs_staffing") {
|
|
203060
|
+
const summary = planning.issues.map((i) => i.label ?? i.message).join("\uFF1B");
|
|
203061
|
+
res.writeHead(400, { "content-type": "application/json" }).end(
|
|
203062
|
+
JSON.stringify({ error: "WORKORDER_NOT_STAFFED", message: `\u4EBA\u5458\u8FD8\u6CA1\u914D\u9F50\uFF1A${summary}`, issues: planning.issues })
|
|
203063
|
+
);
|
|
203064
|
+
return;
|
|
203065
|
+
}
|
|
202869
203066
|
if (targetWs && sessionProjectId) {
|
|
202870
203067
|
await opts.artifactState.upsertWorkspaceBinding({ workOrderId: targetWs, projectId: sessionProjectId });
|
|
202871
203068
|
}
|
|
@@ -205362,6 +205559,7 @@ var init_server3 = __esm({
|
|
|
205362
205559
|
init_validate_principals();
|
|
205363
205560
|
init_assistants();
|
|
205364
205561
|
init_workorder_detail();
|
|
205562
|
+
init_workorders();
|
|
205365
205563
|
init_discuss();
|
|
205366
205564
|
init_planner();
|
|
205367
205565
|
init_chat_parts();
|
|
@@ -207361,6 +207559,8 @@ var init_memory_control_plane_store = __esm({
|
|
|
207361
207559
|
members = /* @__PURE__ */ new Map();
|
|
207362
207560
|
// key = companyId\0accountId
|
|
207363
207561
|
invitations = /* @__PURE__ */ new Map();
|
|
207562
|
+
/** 共享邀请链接:**token → 行**(同公司可并存任意多条;不作废、只过期)。 */
|
|
207563
|
+
inviteLinks = /* @__PURE__ */ new Map();
|
|
207364
207564
|
accounts = /* @__PURE__ */ new Map();
|
|
207365
207565
|
/** protected:FileControlPlaneStore 子类 open() 回放快照时直接恢复(密码须持久化)。 */
|
|
207366
207566
|
accountSecrets = /* @__PURE__ */ new Map();
|
|
@@ -207419,10 +207619,12 @@ var init_memory_control_plane_store = __esm({
|
|
|
207419
207619
|
const prev = this.members.get(key);
|
|
207420
207620
|
const displayName = m2.displayName ?? prev?.displayName;
|
|
207421
207621
|
const onboardingCompletedAt = m2.onboardingCompletedAt ?? prev?.onboardingCompletedAt;
|
|
207622
|
+
const onboardingStep = m2.onboardingStep ?? prev?.onboardingStep;
|
|
207422
207623
|
this.members.set(key, {
|
|
207423
207624
|
...m2,
|
|
207424
207625
|
...displayName !== void 0 ? { displayName } : {},
|
|
207425
|
-
...onboardingCompletedAt !== void 0 ? { onboardingCompletedAt } : {}
|
|
207626
|
+
...onboardingCompletedAt !== void 0 ? { onboardingCompletedAt } : {},
|
|
207627
|
+
...onboardingStep !== void 0 ? { onboardingStep } : {}
|
|
207426
207628
|
});
|
|
207427
207629
|
}
|
|
207428
207630
|
async getMember(companyId, accountId) {
|
|
@@ -207509,11 +207711,24 @@ var init_memory_control_plane_store = __esm({
|
|
|
207509
207711
|
const stored = {
|
|
207510
207712
|
...member,
|
|
207511
207713
|
...prev?.displayName !== void 0 ? { displayName: prev.displayName } : {},
|
|
207512
|
-
...prev?.onboardingCompletedAt !== void 0 ? { onboardingCompletedAt: prev.onboardingCompletedAt } : {}
|
|
207714
|
+
...prev?.onboardingCompletedAt !== void 0 ? { onboardingCompletedAt: prev.onboardingCompletedAt } : {},
|
|
207715
|
+
...prev?.onboardingStep !== void 0 ? { onboardingStep: prev.onboardingStep } : {}
|
|
207513
207716
|
};
|
|
207514
207717
|
this.members.set(key, stored);
|
|
207515
207718
|
return { ok: true, invitation: { ...inv }, member: { ...stored } };
|
|
207516
207719
|
}
|
|
207720
|
+
// —— 共享邀请链接 ——
|
|
207721
|
+
async createInviteLink(link) {
|
|
207722
|
+
if (this.inviteLinks.has(link.token)) throw new Error(`invite link already exists: ${link.token}`);
|
|
207723
|
+
this.inviteLinks.set(link.token, { ...link });
|
|
207724
|
+
}
|
|
207725
|
+
async listInviteLinks(companyId, filter) {
|
|
207726
|
+
return [...this.inviteLinks.values()].filter((l) => l.companyId === companyId && (filter?.createdBy === void 0 || l.createdBy === filter.createdBy)).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt) || a.token.localeCompare(b2.token)).map((l) => ({ ...l }));
|
|
207727
|
+
}
|
|
207728
|
+
async getInviteLinkByToken(token) {
|
|
207729
|
+
const link = this.inviteLinks.get(token);
|
|
207730
|
+
return link ? { ...link } : null;
|
|
207731
|
+
}
|
|
207517
207732
|
// —— 账号 ——
|
|
207518
207733
|
async upsertAccount(a) {
|
|
207519
207734
|
this.accounts.set(a.id, { ...a });
|
|
@@ -207854,6 +208069,13 @@ var init_engine_handoff = __esm({
|
|
|
207854
208069
|
}
|
|
207855
208070
|
});
|
|
207856
208071
|
|
|
208072
|
+
// ../testkit/src/closure-approve.ts
|
|
208073
|
+
var init_closure_approve = __esm({
|
|
208074
|
+
"../testkit/src/closure-approve.ts"() {
|
|
208075
|
+
"use strict";
|
|
208076
|
+
}
|
|
208077
|
+
});
|
|
208078
|
+
|
|
207857
208079
|
// ../testkit/src/pg-test-schemas.ts
|
|
207858
208080
|
var init_pg_test_schemas = __esm({
|
|
207859
208081
|
"../testkit/src/pg-test-schemas.ts"() {
|
|
@@ -207878,6 +208100,7 @@ var init_src9 = __esm({
|
|
|
207878
208100
|
init_memory_actor_memory_store();
|
|
207879
208101
|
init_test_engine();
|
|
207880
208102
|
init_engine_handoff();
|
|
208103
|
+
init_closure_approve();
|
|
207881
208104
|
init_pg_test_schemas();
|
|
207882
208105
|
}
|
|
207883
208106
|
});
|
|
@@ -210947,6 +211170,7 @@ var init_control_plane_file_store = __esm({
|
|
|
210947
211170
|
"createInvitation",
|
|
210948
211171
|
"updateInvitationStatus",
|
|
210949
211172
|
"updateInvitationStatusIf",
|
|
211173
|
+
"createInviteLink",
|
|
210950
211174
|
"commitAcceptedInvitation",
|
|
210951
211175
|
"upsertAccount",
|
|
210952
211176
|
"setAccountPassword",
|
|
@@ -210987,6 +211211,7 @@ var init_control_plane_file_store = __esm({
|
|
|
210987
211211
|
await MemoryControlPlaneStore.prototype.upsertMember.call(store, row);
|
|
210988
211212
|
}
|
|
210989
211213
|
for (const inv of snap.invitations ?? []) await MemoryControlPlaneStore.prototype.createInvitation.call(store, inv);
|
|
211214
|
+
for (const link of snap.inviteLinks ?? []) await MemoryControlPlaneStore.prototype.createInviteLink.call(store, link);
|
|
210990
211215
|
for (const a of snap.accounts ?? []) await MemoryControlPlaneStore.prototype.upsertAccount.call(store, a);
|
|
210991
211216
|
for (const e of snap.accountSecrets ?? []) store.accountSecrets.set(e.accountId, { ...e.secret });
|
|
210992
211217
|
for (const e of snap.digitalEmployees ?? []) await MemoryControlPlaneStore.prototype.upsertDigitalEmployee.call(store, e);
|
|
@@ -211022,6 +211247,8 @@ var init_control_plane_file_store = __esm({
|
|
|
211022
211247
|
companies,
|
|
211023
211248
|
members,
|
|
211024
211249
|
invitations: await this.listInvitations(),
|
|
211250
|
+
// 按 token 存、没有「列全部公司」的读面——直接读 protected map(同 offboardings 的写法)。
|
|
211251
|
+
inviteLinks: [...this.inviteLinks.values()],
|
|
211025
211252
|
accounts,
|
|
211026
211253
|
accountSecrets,
|
|
211027
211254
|
digitalEmployees,
|
|
@@ -220373,7 +220600,7 @@ async function isEmployedElsewhere(store, companyId, accountId) {
|
|
|
220373
220600
|
const memberships = await store.listMembershipsForAccount(accountId);
|
|
220374
220601
|
return memberships.some((m2) => m2.companyId !== companyId);
|
|
220375
220602
|
}
|
|
220376
|
-
var import_node_crypto50, ROLES, INVITABLE_ROLES, INVITATION_TTL_MS, SLUG_RE, CompanyError, CompaniesService;
|
|
220603
|
+
var import_node_crypto50, ROLES, INVITABLE_ROLES, INVITATION_TTL_MS, INVITE_LINK_TTL_MS, INVITE_LINK_TOKEN_BYTES, INVITE_LINK_ROLE, SLUG_RE, CompanyError, CompaniesService;
|
|
220377
220604
|
var init_service6 = __esm({
|
|
220378
220605
|
"../server/src/domains/companies/service.ts"() {
|
|
220379
220606
|
"use strict";
|
|
@@ -220382,6 +220609,9 @@ var init_service6 = __esm({
|
|
|
220382
220609
|
ROLES = ["owner", "admin", "member"];
|
|
220383
220610
|
INVITABLE_ROLES = ["admin", "member"];
|
|
220384
220611
|
INVITATION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
220612
|
+
INVITE_LINK_TTL_MS = INVITATION_TTL_MS;
|
|
220613
|
+
INVITE_LINK_TOKEN_BYTES = 32;
|
|
220614
|
+
INVITE_LINK_ROLE = "member";
|
|
220385
220615
|
SLUG_RE = /^[a-z][a-z0-9-]*$/;
|
|
220386
220616
|
CompanyError = class extends Error {
|
|
220387
220617
|
constructor(status, code2, message, details) {
|
|
@@ -220526,7 +220756,7 @@ var init_service6 = __esm({
|
|
|
220526
220756
|
accountId: creatorAccountId,
|
|
220527
220757
|
actorId: creatorAccountId,
|
|
220528
220758
|
role: "owner",
|
|
220529
|
-
|
|
220759
|
+
onboardingStep: "runtime"
|
|
220530
220760
|
};
|
|
220531
220761
|
await this.store.upsertMember(owner);
|
|
220532
220762
|
return company;
|
|
@@ -220562,9 +220792,14 @@ var init_service6 = __esm({
|
|
|
220562
220792
|
* 这两处方向相反,改动时别顺手一起过滤。
|
|
220563
220793
|
* - 本人查自己走 `getMember`(`GET /api/companies/:id/members/me`),不受本过滤影响——
|
|
220564
220794
|
* 否则「未完成的人」连自己的 membership 都读不到,就没法判断该不该进向导(自我指涉死锁)。
|
|
220795
|
+
*
|
|
220796
|
+
* 🚨 **`role==='owner'` 从本过滤豁免**(2026-09-08 反转 ADR 0208 owner 侧 🚨):owner 建团队后
|
|
220797
|
+
* `onboardingCompletedAt` 要等他走完 owner 流最后一步才落,其间他若停在环境配置步就出去了,
|
|
220798
|
+
* 这条过滤会让**团队 owner 从自己团队的成员页上消失**(判死锁)。他建了公司就是这家公司的
|
|
220799
|
+
* 成员,向导走完与否只影响他自己下次进来落到哪一步,不影响他是不是成员。
|
|
220565
220800
|
*/
|
|
220566
220801
|
async listMemberViews(companyId) {
|
|
220567
|
-
const members = (await this.listMembers(companyId)).filter((m2) => m2.onboardingCompletedAt);
|
|
220802
|
+
const members = (await this.listMembers(companyId)).filter((m2) => m2.onboardingCompletedAt || m2.role === "owner");
|
|
220568
220803
|
const accounts = await Promise.all(members.map((m2) => this.store.getAccount(m2.accountId)));
|
|
220569
220804
|
return members.map((m2, i) => {
|
|
220570
220805
|
const acc = accounts[i];
|
|
@@ -220752,6 +220987,145 @@ var init_service6 = __esm({
|
|
|
220752
220987
|
return {};
|
|
220753
220988
|
}
|
|
220754
220989
|
}
|
|
220990
|
+
// —— 共享邀请链接(Figma 466:217;语义见 contract 的 `CompanyInviteLink`)——
|
|
220991
|
+
/**
|
|
220992
|
+
* 取**我自己**在这家公司当前可用的那条链接;没有、或都过期了返回 `null`。
|
|
220993
|
+
*
|
|
220994
|
+
* 作用域是 `(companyId, createdBy)` 而不是整个公司:链接允许并存任意多条(发起人 2026-09-08
|
|
220995
|
+
* 定案),所以「当前那条」只对**签发人自己**有意义。别人签了几条、什么时候签的,与我这条无关。
|
|
220996
|
+
*
|
|
220997
|
+
* 「过期的当作没有」:链接只有「现在能不能用」这一个问题。把过期行原样吐给页面,页面就得
|
|
220998
|
+
* 再判一次同样的规则——判据一分成两处迟早漂。
|
|
220999
|
+
*/
|
|
221000
|
+
async getMyInviteLink(companyId, createdBy) {
|
|
221001
|
+
const mine = await this.store.listInviteLinks(companyId, { createdBy });
|
|
221002
|
+
return mine.find((l) => !this.isPast(l.expiresAt)) ?? null;
|
|
221003
|
+
}
|
|
221004
|
+
/**
|
|
221005
|
+
* 组织页弹窗打开时调:**幂等**拿到我自己的当前链接,没有才签一条。
|
|
221006
|
+
*
|
|
221007
|
+
* 做成幂等而不是「每次打开都签一条新的」,是为了让同一个人重复开弹窗看到同一把钥匙——
|
|
221008
|
+
* 否则每开一次弹窗就往库里多塞一条,而人只会复制走最后看到的那条。**这不是「一公司一条」
|
|
221009
|
+
* 那个约束的残留**:那条约束管的是「别人签会不会把我的打死」(已取消),这里管的是
|
|
221010
|
+
* 「我自己重复打开会不会每次都换」。
|
|
221011
|
+
*/
|
|
221012
|
+
async ensureMyInviteLink(companyId, createdBy) {
|
|
221013
|
+
await this.requireCompany(companyId);
|
|
221014
|
+
const current = await this.getMyInviteLink(companyId, createdBy);
|
|
221015
|
+
if (current) return current;
|
|
221016
|
+
return this.issueInviteLink(companyId, createdBy);
|
|
221017
|
+
}
|
|
221018
|
+
/**
|
|
221019
|
+
* 再签一条**新的**链接。
|
|
221020
|
+
*
|
|
221021
|
+
* ⚠ 与 Onboarding PRD §4.3「重新生成 → 旧链接立即失效」**相悖**,是发起人 2026-09-08 明确
|
|
221022
|
+
* 推翻的那条:签新的**不作废**任何已发出去的链接(自己的、别人的都不动)。所以这个动作
|
|
221023
|
+
* 不是破坏性的,也就不需要二次确认。
|
|
221024
|
+
*
|
|
221025
|
+
* 没有「吊销某条链接」的出口——真要收回得另加吊销位,本轮不做(见 ADR 0513 D3)。
|
|
221026
|
+
*/
|
|
221027
|
+
async issueAnotherInviteLink(companyId, createdBy) {
|
|
221028
|
+
await this.requireCompany(companyId);
|
|
221029
|
+
return this.issueInviteLink(companyId, createdBy);
|
|
221030
|
+
}
|
|
221031
|
+
async issueInviteLink(companyId, createdBy) {
|
|
221032
|
+
const nowMs = Date.parse(this.now());
|
|
221033
|
+
const link = {
|
|
221034
|
+
companyId,
|
|
221035
|
+
token: (0, import_node_crypto50.randomBytes)(INVITE_LINK_TOKEN_BYTES).toString("base64url"),
|
|
221036
|
+
role: INVITE_LINK_ROLE,
|
|
221037
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
221038
|
+
expiresAt: new Date(nowMs + INVITE_LINK_TTL_MS).toISOString(),
|
|
221039
|
+
...createdBy !== void 0 ? { createdBy } : {}
|
|
221040
|
+
};
|
|
221041
|
+
await this.store.createInviteLink(link);
|
|
221042
|
+
return link;
|
|
221043
|
+
}
|
|
221044
|
+
/** 这条链接对外的完整地址;本实例没配 `OASIS_CONSOLE_URL` 时返回 `undefined`(调用方不放链接)。 */
|
|
221045
|
+
inviteLinkUrl(token) {
|
|
221046
|
+
return inviteLinkJoinUrl(this.consoleUrl, token);
|
|
221047
|
+
}
|
|
221048
|
+
/**
|
|
221049
|
+
* 链接的只读预览(`GET /api/invite-links/:token`)——接受邀请页进页面时那一问。
|
|
221050
|
+
* **一个字节都不写**:不建邀请行、不改任何状态。真正的兑换在 accept 那一步。
|
|
221051
|
+
*
|
|
221052
|
+
* 错误与邀请预览对齐,页面判定层(`invite-view.ts`)只认 details,不认状态码:
|
|
221053
|
+
* 已经是这家公司的人 → 409 + `alreadyMember`(PRD §7.2:「已经是成员:直接进入该团队空间,
|
|
221054
|
+
* 不重复加入」,这是**正常路径**,绝不能渲染成「链接已失效」);过期 → 410(前端给
|
|
221055
|
+
* 「邀请链接已失效」那一屏);token 查不到 → 404。
|
|
221056
|
+
*/
|
|
221057
|
+
async previewInviteLink(token, viewer) {
|
|
221058
|
+
const link = await this.requireUsableInviteLink(token, viewer.accountId);
|
|
221059
|
+
const company = await this.store.getCompany(link.companyId);
|
|
221060
|
+
return {
|
|
221061
|
+
token: link.token,
|
|
221062
|
+
companyId: link.companyId,
|
|
221063
|
+
companyName: company?.name ?? link.companyId,
|
|
221064
|
+
role: link.role,
|
|
221065
|
+
expiresAt: link.expiresAt,
|
|
221066
|
+
...await this.inviterNameOf({ companyId: link.companyId, invitedBy: link.createdBy })
|
|
221067
|
+
};
|
|
221068
|
+
}
|
|
221069
|
+
/**
|
|
221070
|
+
* 兑换:把一条共享链接换成**属于当前登录邮箱**的 `CompanyInvitation`,随后交给既有的
|
|
221071
|
+
* peek → 翻活/物化 → commit 流水线消费。
|
|
221072
|
+
*
|
|
221073
|
+
* 为什么绕这一道而不让 accept 直接吃 token:那条流水线的每一步都建立在「这条邀请是发给我的、
|
|
221074
|
+
* 接受一次就消费掉」之上(CO-005 唯一 pending、`CAS(pending→accepted)`、失败补偿全按单条邀请写)。
|
|
221075
|
+
* 共享链接是**多人多次**可用的,直接塞进去就得把那些不变量逐条改成「视 kind 而定」——两种语义
|
|
221076
|
+
* 挤在一张表里,每条 CAS 都要多一个分支,那才是真正会出事的地方。换成「链接 → 我的那一条邀请」
|
|
221077
|
+
* 之后,链接侧只管「谁可以进」,邀请侧仍然只有一种语义。
|
|
221078
|
+
*
|
|
221079
|
+
* **复用已有 pending**:同邮箱已经有一条未过期的 pending(比如管理员先前按邮箱单独邀请过他、
|
|
221080
|
+
* 或者他点过**另一条**链接但没走完),就直接用那条——既不违反 CO-005,也让「按邮箱邀请」与
|
|
221081
|
+
* 「任意多条共享链接」全部自然汇流到同一条邀请上;那条邀请的 role 优先(管理员显式给的 admin
|
|
221082
|
+
* 不该被链接的默认 member 冲掉)。
|
|
221083
|
+
*/
|
|
221084
|
+
async claimInviteLink(token, claimer) {
|
|
221085
|
+
const link = await this.requireUsableInviteLink(token, claimer.accountId);
|
|
221086
|
+
const mail = claimer.email?.trim().toLowerCase() ?? "";
|
|
221087
|
+
if (!mail) throw new CompanyError(403, "EMAIL_REQUIRED", "\u5F53\u524D\u767B\u5F55\u8EAB\u4EFD\u6CA1\u6709\u90AE\u7BB1\uFF0C\u65E0\u6CD5\u7528\u9080\u8BF7\u94FE\u63A5\u52A0\u5165");
|
|
221088
|
+
const existing = await this.store.listInvitations({ companyId: link.companyId, email: mail, status: "pending" });
|
|
221089
|
+
const reusable = existing.find((inv) => !this.isExpired(inv));
|
|
221090
|
+
if (reusable) return reusable;
|
|
221091
|
+
const invitation = {
|
|
221092
|
+
id: `invitation:${(0, import_node_crypto50.randomUUID)()}`,
|
|
221093
|
+
companyId: link.companyId,
|
|
221094
|
+
email: mail,
|
|
221095
|
+
role: link.role,
|
|
221096
|
+
status: "pending",
|
|
221097
|
+
// 有效期跟着**这条链接**走:链接死了,由它换出来的邀请不该比它活得更久。
|
|
221098
|
+
expiresAt: link.expiresAt,
|
|
221099
|
+
...link.createdBy !== void 0 ? { invitedBy: link.createdBy } : {}
|
|
221100
|
+
};
|
|
221101
|
+
await this.store.createInvitation(invitation);
|
|
221102
|
+
return invitation;
|
|
221103
|
+
}
|
|
221104
|
+
/**
|
|
221105
|
+
* 链接可用性的三道闸(预览与兑换共用,保证两边**逐字**同一套判定):
|
|
221106
|
+
* 存在 → 未过期 → 我还不是这家公司的人。
|
|
221107
|
+
*/
|
|
221108
|
+
async requireUsableInviteLink(token, accountId) {
|
|
221109
|
+
const link = await this.store.getInviteLinkByToken(token);
|
|
221110
|
+
if (!link) throw new CompanyError(404, "NOT_FOUND", "\u9080\u8BF7\u94FE\u63A5\u65E0\u6548");
|
|
221111
|
+
if (this.isPast(link.expiresAt)) throw new CompanyError(410, "EXPIRED", "\u9080\u8BF7\u94FE\u63A5\u5DF2\u8FC7\u671F", await this.joinDetails(link, accountId));
|
|
221112
|
+
const member = await this.store.getMember(link.companyId, accountId);
|
|
221113
|
+
if (member) {
|
|
221114
|
+
throw new CompanyError(409, "ALREADY_MEMBER", "\u4F60\u5DF2\u7ECF\u662F\u8FD9\u4E2A\u56E2\u961F\u7684\u6210\u5458", await this.joinDetails(link, accountId));
|
|
221115
|
+
}
|
|
221116
|
+
return link;
|
|
221117
|
+
}
|
|
221118
|
+
/** 链接类错误的 details——与 `decorateInvitationError` 同一组键,接受邀请页据它分「已加入」与「已失效」。 */
|
|
221119
|
+
async joinDetails(link, accountId) {
|
|
221120
|
+
const company = await this.store.getCompany(link.companyId).catch(() => null);
|
|
221121
|
+
const member = await this.store.getMember(link.companyId, accountId).catch(() => null);
|
|
221122
|
+
return {
|
|
221123
|
+
companyId: link.companyId,
|
|
221124
|
+
companyName: company?.name ?? link.companyId,
|
|
221125
|
+
alreadyMember: member !== null,
|
|
221126
|
+
...member?.onboardingCompletedAt ? { onboardingCompletedAt: member.onboardingCompletedAt } : {}
|
|
221127
|
+
};
|
|
221128
|
+
}
|
|
220755
221129
|
/**
|
|
220756
221130
|
* 落「新手指引已完成」(`POST /api/companies/:id/members/me/onboarding-complete` 的服务层)。
|
|
220757
221131
|
*
|
|
@@ -220769,6 +221143,29 @@ var init_service6 = __esm({
|
|
|
220769
221143
|
await this.store.upsertMember(next);
|
|
220770
221144
|
return next;
|
|
220771
221145
|
}
|
|
221146
|
+
/**
|
|
221147
|
+
* 记「新手指引走到了哪一步」(`POST /api/companies/:id/members/me/onboarding-step` 的服务层,
|
|
221148
|
+
* 2026-09-08 反转 ADR 0208 §4)。
|
|
221149
|
+
*
|
|
221150
|
+
* **本人限定**:路径写死 `me`,只对自己那条 membership 生效;不是成员 → 404。
|
|
221151
|
+
* **可向前改写**:与 `onboardingCompletedAt` 单向语义不同,本列是进度指针;store 的 upsert
|
|
221152
|
+
* 对本列走 COALESCE(传值即覆盖,传 null 不动)。
|
|
221153
|
+
* **上报是尽力而为**:路由层若拿到非 200/非 404 也不该阻塞用户进下一步(前端做,不在这里判)。
|
|
221154
|
+
*
|
|
221155
|
+
* 传入的 `step` 值域约束在契约层是 `OnboardingStepId`(team / runtime / assistant),但存储层
|
|
221156
|
+
* 只把它当字符串——放宽到「非空 trim 串」即可,非法值不落库。这是一条进度指针,不是权限判据;
|
|
221157
|
+
* 万一将来加了新步骤名,老服务端也能透传,读侧再降级到兜底回落。
|
|
221158
|
+
*/
|
|
221159
|
+
async setMemberOnboardingStep(companyId, accountId, step) {
|
|
221160
|
+
const trimmed = step?.trim();
|
|
221161
|
+
if (!trimmed) throw new CompanyError(400, "BAD_REQUEST", "step \u4E0D\u80FD\u4E3A\u7A7A");
|
|
221162
|
+
await this.requireCompany(companyId);
|
|
221163
|
+
const member = await this.store.getMember(companyId, accountId);
|
|
221164
|
+
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
221165
|
+
const next = { ...member, onboardingStep: trimmed };
|
|
221166
|
+
await this.store.upsertMember(next);
|
|
221167
|
+
return next;
|
|
221168
|
+
}
|
|
220772
221169
|
/**
|
|
220773
221170
|
* 列出发给某邮箱的有效(pending 未过期)邀请;顺带把过期的 pending 落为 expired。
|
|
220774
221171
|
*
|
|
@@ -220891,7 +221288,11 @@ var init_service6 = __esm({
|
|
|
220891
221288
|
}
|
|
220892
221289
|
}
|
|
220893
221290
|
isExpired(inv) {
|
|
220894
|
-
return
|
|
221291
|
+
return this.isPast(inv.expiresAt);
|
|
221292
|
+
}
|
|
221293
|
+
/** 统一时间源下「这个时刻过去了没有」。邀请与链接共用,避免两处各写一次比较。 */
|
|
221294
|
+
isPast(at) {
|
|
221295
|
+
return Date.parse(at) <= Date.parse(this.now());
|
|
220895
221296
|
}
|
|
220896
221297
|
async requireCompany(companyId) {
|
|
220897
221298
|
const c = await this.store.getCompany(companyId);
|
|
@@ -221274,6 +221675,15 @@ function companiesDomain(opts) {
|
|
|
221274
221675
|
const member = await service.completeMemberOnboarding(req.params.id, me.accountId).catch(mapErr);
|
|
221275
221676
|
return { status: 200, body: member };
|
|
221276
221677
|
});
|
|
221678
|
+
router.post("/api/companies/:id/members/me/onboarding-step", async (req) => {
|
|
221679
|
+
const me = await whoami(req);
|
|
221680
|
+
const body2 = req.body ?? {};
|
|
221681
|
+
if (typeof body2.step !== "string") {
|
|
221682
|
+
throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 step: string");
|
|
221683
|
+
}
|
|
221684
|
+
const member = await service.setMemberOnboardingStep(req.params.id, me.accountId, body2.step).catch(mapErr);
|
|
221685
|
+
return { status: 200, body: member };
|
|
221686
|
+
});
|
|
221277
221687
|
router.patch("/api/companies/:id/members/:accountId", async (req) => {
|
|
221278
221688
|
const b2 = req.body ?? {};
|
|
221279
221689
|
const cid = req.params.id;
|
|
@@ -221407,6 +221817,10 @@ function companiesDomain(opts) {
|
|
|
221407
221817
|
const result = await off.commit(cid, req.params.accountId, req.params.operationId, version2).catch(mapErr);
|
|
221408
221818
|
return { status: 200, body: result };
|
|
221409
221819
|
});
|
|
221820
|
+
const toInviteLinkBody = (link) => {
|
|
221821
|
+
const url = service.inviteLinkUrl(link.token);
|
|
221822
|
+
return { token: link.token, role: link.role, createdAt: link.createdAt, expiresAt: link.expiresAt, ...url ? { url } : {} };
|
|
221823
|
+
};
|
|
221410
221824
|
router.post("/api/companies/:id/invitations", async (req) => {
|
|
221411
221825
|
const b2 = req.body ?? {};
|
|
221412
221826
|
if (!b2.email) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 email");
|
|
@@ -221425,9 +221839,7 @@ function companiesDomain(opts) {
|
|
|
221425
221839
|
const items = me.email !== void 0 ? await service.listInvitationsForEmail(me.email) : [];
|
|
221426
221840
|
return { status: 200, body: { items } };
|
|
221427
221841
|
});
|
|
221428
|
-
|
|
221429
|
-
const me = await whoami(req);
|
|
221430
|
-
const inv = await service.peekAcceptableInvitation(req.params.invitationId, me).catch(mapErr);
|
|
221842
|
+
const acceptPeekedInvitation = async (req, inv, me) => {
|
|
221431
221843
|
let revivedSnapshot = null;
|
|
221432
221844
|
let reviveSeq = 0;
|
|
221433
221845
|
let materializedSnapshot = null;
|
|
@@ -221451,8 +221863,7 @@ function companiesDomain(opts) {
|
|
|
221451
221863
|
}
|
|
221452
221864
|
}
|
|
221453
221865
|
try {
|
|
221454
|
-
|
|
221455
|
-
return { status: 201, body: member };
|
|
221866
|
+
return await service.commitAcceptedInvitation(inv, me).catch(mapErr);
|
|
221456
221867
|
} catch (err) {
|
|
221457
221868
|
if (revivedSnapshot && actorsSvc) {
|
|
221458
221869
|
let confirmedAbsent = false;
|
|
@@ -221508,6 +221919,36 @@ function companiesDomain(opts) {
|
|
|
221508
221919
|
}
|
|
221509
221920
|
throw err;
|
|
221510
221921
|
}
|
|
221922
|
+
};
|
|
221923
|
+
router.post("/api/invitations/:invitationId/accept", async (req) => {
|
|
221924
|
+
const me = await whoami(req);
|
|
221925
|
+
const inv = await service.peekAcceptableInvitation(req.params.invitationId, me).catch(mapErr);
|
|
221926
|
+
return { status: 201, body: await acceptPeekedInvitation(req, inv, me) };
|
|
221927
|
+
});
|
|
221928
|
+
router.post("/api/companies/:id/invite-links/mine", async (req) => {
|
|
221929
|
+
const cid = req.params.id;
|
|
221930
|
+
await requireOwnerOrAdmin(req, cid);
|
|
221931
|
+
const me = await whoami(req);
|
|
221932
|
+
const link = await service.ensureMyInviteLink(cid, me.accountId).catch(mapErr);
|
|
221933
|
+
return { status: 200, body: toInviteLinkBody(link) };
|
|
221934
|
+
});
|
|
221935
|
+
router.post("/api/companies/:id/invite-links", async (req) => {
|
|
221936
|
+
const cid = req.params.id;
|
|
221937
|
+
await requireOwnerOrAdmin(req, cid);
|
|
221938
|
+
const me = await whoami(req);
|
|
221939
|
+
const link = await service.issueAnotherInviteLink(cid, me.accountId).catch(mapErr);
|
|
221940
|
+
return { status: 201, body: toInviteLinkBody(link) };
|
|
221941
|
+
});
|
|
221942
|
+
router.get("/api/invite-links/:token", async (req) => {
|
|
221943
|
+
const me = await whoami(req);
|
|
221944
|
+
const preview2 = await service.previewInviteLink(req.params.token, me).catch(mapErr);
|
|
221945
|
+
return { status: 200, body: preview2 };
|
|
221946
|
+
});
|
|
221947
|
+
router.post("/api/invite-links/:token/accept", async (req) => {
|
|
221948
|
+
const me = await whoami(req);
|
|
221949
|
+
const claimed = await service.claimInviteLink(req.params.token, me).catch(mapErr);
|
|
221950
|
+
const inv = await service.peekAcceptableInvitation(claimed.id, me).catch(mapErr);
|
|
221951
|
+
return { status: 201, body: await acceptPeekedInvitation(req, inv, me) };
|
|
221511
221952
|
});
|
|
221512
221953
|
router.post("/api/invitations/:invitationId/decline", async (req) => {
|
|
221513
221954
|
await service.declineInvitation(req.params.invitationId).catch(mapErr);
|
|
@@ -222296,16 +222737,21 @@ Write-Host " Stop : Stop-ScheduledTask -TaskName 'OasisNode'"
|
|
|
222296
222737
|
Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode' -Confirm:$false"
|
|
222297
222738
|
`;
|
|
222298
222739
|
}
|
|
222740
|
+
function daemonNpmPrefix(pkg = DAEMON_NPM_PACKAGE) {
|
|
222741
|
+
return pkg === LEGACY_DAEMON_NPM_PACKAGE ? "~/.oasis/npm-global" : `~/.oasis/npm-global/${pkg}`;
|
|
222742
|
+
}
|
|
222299
222743
|
function buildNpxCmd(p2) {
|
|
222300
222744
|
const namePart = p2.nodeName ? ` --name ${shellQuote(p2.nodeName)}` : "";
|
|
222301
222745
|
const instanceName = connectInstanceName(p2.gatewayUrl, p2.token, p2.companyId);
|
|
222302
222746
|
const companyPart = p2.companyId ? ` --company ${shellQuote(p2.companyId)}` : "";
|
|
222303
|
-
|
|
222747
|
+
const prefix = daemonNpmPrefix();
|
|
222748
|
+
return `npm install -g --prefer-online --prefix ${prefix} ${DAEMON_NPM_PACKAGE}@latest && ${prefix}/bin/oasis daemon start --instance ${shellQuote(instanceName)} --server-ws ${shellQuote(p2.gatewayUrl)}${companyPart}${namePart} --enroll-token ${shellQuote(p2.token)}`;
|
|
222304
222749
|
}
|
|
222305
222750
|
var import_node_crypto53;
|
|
222306
222751
|
var init_connect_script = __esm({
|
|
222307
222752
|
"../server/src/domains/nodes/connect-script.ts"() {
|
|
222308
222753
|
"use strict";
|
|
222754
|
+
init_src();
|
|
222309
222755
|
import_node_crypto53 = require("node:crypto");
|
|
222310
222756
|
}
|
|
222311
222757
|
});
|
|
@@ -222595,6 +223041,7 @@ function nodesDomain(deps) {
|
|
|
222595
223041
|
const hub = deps.getHub();
|
|
222596
223042
|
if (hub) wireHub(hub);
|
|
222597
223043
|
const daemonVersionById = new Map((hub?.connectedDaemons() ?? []).map((d) => [d.daemonId, d.meta.daemonVersion]));
|
|
223044
|
+
const daemonPkgById = new Map((hub?.connectedDaemons() ?? []).map((d) => [d.daemonId, d.meta.daemonPkg]));
|
|
222598
223045
|
const connectionById = new Map((hub?.recentConnectionChanges() ?? []).map((d) => [d.daemonId, d]));
|
|
222599
223046
|
const onlineIds = new Set((hub?.connectedDaemons() ?? []).map((d) => d.daemonId));
|
|
222600
223047
|
const healthNow = Date.now();
|
|
@@ -222624,6 +223071,7 @@ function nodesDomain(deps) {
|
|
|
222624
223071
|
runtimes: rts.map(runtimeInfoOf),
|
|
222625
223072
|
nodeVersion: n.nodeVersion,
|
|
222626
223073
|
...daemonVersion ? { daemonVersion } : {},
|
|
223074
|
+
...daemonPkgById.get(n.id) ? { daemonPkg: daemonPkgById.get(n.id) } : {},
|
|
222627
223075
|
...n.name ? { name: n.name } : {},
|
|
222628
223076
|
...updateStatus ? { updateStatus } : {},
|
|
222629
223077
|
...connection ? {
|
|
@@ -222788,12 +223236,14 @@ function nodesDomain(deps) {
|
|
|
222788
223236
|
if (!daemon) return { status: 409, body: { error: `\u8282\u70B9 ${nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF\uFF0C\u65E0\u6CD5\u66F4\u65B0` } };
|
|
222789
223237
|
const current = daemon.meta.daemonVersion;
|
|
222790
223238
|
const { version: latest } = await resolveLatestDaemonVersion();
|
|
222791
|
-
|
|
223239
|
+
const nodePkg = daemon.meta.daemonPkg;
|
|
223240
|
+
const sameLane = !nodePkg || nodePkg === DAEMON_NPM_PACKAGE;
|
|
223241
|
+
if (sameLane && latest && current && !daemonVersionLess(current, latest)) {
|
|
222792
223242
|
nodeUpdates.delete(nodeId);
|
|
222793
223243
|
return { status: 200, body: { ok: true, alreadyLatest: true, version: current } };
|
|
222794
223244
|
}
|
|
222795
223245
|
const activeRunCount = deps.activeRunCountOf?.(nodeId) ?? 0;
|
|
222796
|
-
const sent = hub.dispatch(nodeId, { type: "update" });
|
|
223246
|
+
const sent = hub.dispatch(nodeId, { type: "update", pkg: DAEMON_NPM_PACKAGE });
|
|
222797
223247
|
if (!sent) return { status: 409, body: { error: `\u8282\u70B9 ${nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF\uFF0C\u65E0\u6CD5\u66F4\u65B0` } };
|
|
222798
223248
|
const nowMs = Date.now();
|
|
222799
223249
|
const tracked = activeRunCount > 0 ? { ...current ? { fromVersion: current } : {}, requestedAtMs: nowMs, state: "queued" } : { ...current ? { fromVersion: current } : {}, requestedAtMs: nowMs, state: "updating", startedAtMs: nowMs };
|
|
@@ -222889,6 +223339,7 @@ var init_routes7 = __esm({
|
|
|
222889
223339
|
import_node_fs13 = require("node:fs");
|
|
222890
223340
|
import_node_url6 = require("node:url");
|
|
222891
223341
|
import_node_path20 = require("node:path");
|
|
223342
|
+
init_src();
|
|
222892
223343
|
init_src7();
|
|
222893
223344
|
init_connect_script();
|
|
222894
223345
|
init_node_health();
|
|
@@ -223607,9 +224058,10 @@ function buildWorkorderActivity(input) {
|
|
|
223607
224058
|
const helperId = managerId ?? ownerId;
|
|
223608
224059
|
const body2 = str4(ev.body) ?? "";
|
|
223609
224060
|
if (isEscalation) {
|
|
223610
|
-
const target = recipients[0] ?? ownerId;
|
|
223611
224061
|
const linkedGapId = str4(ev.gapId);
|
|
223612
224062
|
const stuckActor = (linkedGapId ? issueById.get(linkedGapId)?.authorActorId : void 0) ?? (nodeId ? nodeById.get(nodeId)?.assigneeActorId : void 0) ?? authorId;
|
|
224063
|
+
const selfRef = (id) => !!id && (id === stuckActor || id === authorId);
|
|
224064
|
+
const target = [recipients[0], ownerId, managerId].find((cand) => cand && !selfRef(cand)) ?? recipients[0] ?? ownerId;
|
|
223613
224065
|
issueCards.set(`${issueId}:raised`, {
|
|
223614
224066
|
id: `escalation:${issueId}:raised`,
|
|
223615
224067
|
seq: rec.seq,
|
|
@@ -223633,9 +224085,21 @@ function buildWorkorderActivity(input) {
|
|
|
223633
224085
|
...rec.handActorId ? { handActorId: rec.handActorId } : {},
|
|
223634
224086
|
phase: "not_started",
|
|
223635
224087
|
status: ACTIVITY_COPY.escalationPending(nameOf(stuckActor), nameOf(target)),
|
|
223636
|
-
|
|
224088
|
+
/* 「去处理」进这条上报自己的**讨论房**(`discuss_escalation`),不是只读陈列面。
|
|
224089
|
+
上报的收件人就是必须开口回答的人——给他一个读不了回不了的落点等于死路。
|
|
224090
|
+
|
|
224091
|
+
房间**不在这里造**:前端拿这个 `target`(escalationId)去打
|
|
224092
|
+
`POST /api/inbox/discuss`,那个端点按项幂等——存量复用、没有则建(决策 0027 D3 /
|
|
224093
|
+
0030 D3,ADR 0079 D1「一上报一房」)。1.0 的上报气泡走的就是这条路,所以
|
|
224094
|
+
**存量上报不需要回填**:谁点谁就建出来。投影侧只负责说清"去哪条上报"。 */
|
|
224095
|
+
/* 上报正文**不上卡**——与缺口 / 回压同一条口径(见下面那支的注释)。
|
|
224096
|
+
此前这里把整封信塞进展开区,`ws:wo-635d7add` 那条渲染出的是一屏字:
|
|
224097
|
+
「需要您确认一件事…背景…请您提供以下任意一项…—— 技术细节…[关联缺口] gap:artifact:…」,
|
|
224098
|
+
连内部 artifact id 都印在卡面上。原型两帧口径一致(`1979:3039` 与 `1923:3319` 那张
|
|
224099
|
+
`Rectangle 20`,高 104 / 99):**标题行 + 一句小状态 + 去处理**,没有展开区。
|
|
224100
|
+
全文的去处,就是那个「去处理」按钮。 */
|
|
223637
224101
|
artifacts: [],
|
|
223638
|
-
actions: [{ kind: "
|
|
224102
|
+
actions: [{ kind: "discuss_escalation", label: BUTTON.handle, target: issueId }]
|
|
223639
224103
|
});
|
|
223640
224104
|
break;
|
|
223641
224105
|
}
|
|
@@ -238039,7 +238503,7 @@ var init_pool = __esm({
|
|
|
238039
238503
|
});
|
|
238040
238504
|
|
|
238041
238505
|
// ../storage/src/postgres-control-plane.ts
|
|
238042
|
-
var ident6, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToMember, rowToInvitation, rowToAccount, rowToVerificationCode;
|
|
238506
|
+
var ident6, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
|
|
238043
238507
|
var init_postgres_control_plane = __esm({
|
|
238044
238508
|
"../storage/src/postgres-control-plane.ts"() {
|
|
238045
238509
|
"use strict";
|
|
@@ -238091,6 +238555,16 @@ var init_postgres_control_plane = __esm({
|
|
|
238091
238555
|
await pool.query(`CREATE INDEX IF NOT EXISTS company_invitations_email_idx ON "${s2}".company_invitations (email)`);
|
|
238092
238556
|
await pool.query(`CREATE INDEX IF NOT EXISTS company_invitations_company_idx ON "${s2}".company_invitations (company_id)`);
|
|
238093
238557
|
await pool.query(`
|
|
238558
|
+
CREATE TABLE IF NOT EXISTS "${s2}".company_invite_links (
|
|
238559
|
+
token text PRIMARY KEY,
|
|
238560
|
+
company_id text NOT NULL,
|
|
238561
|
+
role text NOT NULL, -- admin | member\uFF08\u4E0D\u542B owner\uFF09
|
|
238562
|
+
created_by text, -- \u751F\u6210\u4EBA\u8D26\u53F7\uFF1BNULL \u2192 \u63A5\u53D7\u9875\u964D\u7EA7\u4E3A\u4E0D\u663E\u793A\u9080\u8BF7\u4EBA
|
|
238563
|
+
created_at timestamptz NOT NULL,
|
|
238564
|
+
expires_at timestamptz NOT NULL
|
|
238565
|
+
)`);
|
|
238566
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS company_invite_links_company_creator_idx ON "${s2}".company_invite_links (company_id, created_by, created_at DESC)`);
|
|
238567
|
+
await pool.query(`
|
|
238094
238568
|
CREATE TABLE IF NOT EXISTS "${s2}".accounts (
|
|
238095
238569
|
id text PRIMARY KEY,
|
|
238096
238570
|
email text NOT NULL,
|
|
@@ -238137,6 +238611,7 @@ var init_postgres_control_plane = __esm({
|
|
|
238137
238611
|
client.release();
|
|
238138
238612
|
}
|
|
238139
238613
|
}
|
|
238614
|
+
await pool.query(`ALTER TABLE "${s2}".company_members ADD COLUMN IF NOT EXISTS onboarding_step text`);
|
|
238140
238615
|
await pool.query(`CREATE INDEX IF NOT EXISTS company_members_account_company_idx ON "${s2}".company_members (account_id, company_id)`);
|
|
238141
238616
|
await pool.query(`CREATE INDEX IF NOT EXISTS company_members_company_role_idx ON "${s2}".company_members (company_id, role)`);
|
|
238142
238617
|
await pool.query(`
|
|
@@ -238293,14 +238768,15 @@ var init_postgres_control_plane = __esm({
|
|
|
238293
238768
|
// —— 成员 ——
|
|
238294
238769
|
async upsertMember(m2) {
|
|
238295
238770
|
await this.pool.query(
|
|
238296
|
-
`INSERT INTO ${this.s}.company_members (company_id, account_id, actor_id, role, display_name, onboarding_completed_at, created_at, updated_at)
|
|
238297
|
-
VALUES ($1,$2,$3,$4,$5,$6,now(),now())
|
|
238771
|
+
`INSERT INTO ${this.s}.company_members (company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step, created_at, updated_at)
|
|
238772
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,now(),now())
|
|
238298
238773
|
ON CONFLICT (company_id, account_id) DO UPDATE
|
|
238299
238774
|
SET actor_id = $3, role = $4,
|
|
238300
238775
|
display_name = COALESCE($5, company_members.display_name),
|
|
238301
238776
|
onboarding_completed_at = COALESCE($6, company_members.onboarding_completed_at),
|
|
238777
|
+
onboarding_step = COALESCE($7, company_members.onboarding_step),
|
|
238302
238778
|
updated_at = now()`,
|
|
238303
|
-
[m2.companyId, m2.accountId, m2.actorId, m2.role, m2.displayName ?? null, m2.onboardingCompletedAt ?? null]
|
|
238779
|
+
[m2.companyId, m2.accountId, m2.actorId, m2.role, m2.displayName ?? null, m2.onboardingCompletedAt ?? null, m2.onboardingStep ?? null]
|
|
238304
238780
|
);
|
|
238305
238781
|
}
|
|
238306
238782
|
async getMember(companyId, accountId) {
|
|
@@ -238328,7 +238804,7 @@ var init_postgres_control_plane = __esm({
|
|
|
238328
238804
|
const all2 = await client.query(
|
|
238329
238805
|
// 列要写全:只 SELECT 四列的话,返回给调用方的 Membership 会**丢掉** display_name /
|
|
238330
238806
|
// onboarding_completed_at——PATCH 改角色的响应体里昵称凭空消失,而前端拿它当 membership 用。
|
|
238331
|
-
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at
|
|
238807
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
238332
238808
|
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
238333
238809
|
[companyId]
|
|
238334
238810
|
);
|
|
@@ -238366,7 +238842,7 @@ var init_postgres_control_plane = __esm({
|
|
|
238366
238842
|
const all2 = await client.query(
|
|
238367
238843
|
// 列要写全:只 SELECT 四列的话,返回给调用方的 Membership 会**丢掉** display_name /
|
|
238368
238844
|
// onboarding_completed_at——PATCH 改角色的响应体里昵称凭空消失,而前端拿它当 membership 用。
|
|
238369
|
-
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at
|
|
238845
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
238370
238846
|
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
238371
238847
|
[companyId]
|
|
238372
238848
|
);
|
|
@@ -238488,6 +238964,31 @@ var init_postgres_control_plane = __esm({
|
|
|
238488
238964
|
client.release();
|
|
238489
238965
|
}
|
|
238490
238966
|
}
|
|
238967
|
+
// —— 共享邀请链接 ——
|
|
238968
|
+
async createInviteLink(link) {
|
|
238969
|
+
await this.pool.query(
|
|
238970
|
+
`INSERT INTO ${this.s}.company_invite_links (token, company_id, role, created_by, created_at, expires_at)
|
|
238971
|
+
VALUES ($1,$2,$3,$4,$5,$6)`,
|
|
238972
|
+
[link.token, link.companyId, link.role, link.createdBy ?? null, link.createdAt, link.expiresAt]
|
|
238973
|
+
);
|
|
238974
|
+
}
|
|
238975
|
+
async listInviteLinks(companyId, filter) {
|
|
238976
|
+
const args = [companyId];
|
|
238977
|
+
let where = "company_id = $1";
|
|
238978
|
+
if (filter?.createdBy !== void 0) {
|
|
238979
|
+
args.push(filter.createdBy);
|
|
238980
|
+
where += ` AND created_by = $${args.length}`;
|
|
238981
|
+
}
|
|
238982
|
+
const r = await this.pool.query(
|
|
238983
|
+
`SELECT * FROM ${this.s}.company_invite_links WHERE ${where} ORDER BY created_at DESC, token ASC`,
|
|
238984
|
+
args
|
|
238985
|
+
);
|
|
238986
|
+
return r.rows.map(rowToInviteLink);
|
|
238987
|
+
}
|
|
238988
|
+
async getInviteLinkByToken(token) {
|
|
238989
|
+
const r = await this.pool.query(`SELECT * FROM ${this.s}.company_invite_links WHERE token = $1`, [token]);
|
|
238990
|
+
return r.rows[0] ? rowToInviteLink(r.rows[0]) : null;
|
|
238991
|
+
}
|
|
238491
238992
|
// —— 账号 ——
|
|
238492
238993
|
async upsertAccount(a) {
|
|
238493
238994
|
await this.pool.query(
|
|
@@ -238850,8 +239351,10 @@ var init_postgres_control_plane = __esm({
|
|
|
238850
239351
|
role: row.role,
|
|
238851
239352
|
// expand 阶段可空:存量行未回填 → 不带该键(与内存实现一致,读侧统一按 undefined 降级)。
|
|
238852
239353
|
...row.display_name != null ? { displayName: row.display_name } : {},
|
|
238853
|
-
// 空 =
|
|
238854
|
-
...row.onboarding_completed_at != null ? { onboardingCompletedAt: toIso(row.onboarding_completed_at) } : {}
|
|
239354
|
+
// 空 = 还没走完新手指引(= 不算真正加入)。timestamptz → ISO 串,与其它时间字段一致。
|
|
239355
|
+
...row.onboarding_completed_at != null ? { onboardingCompletedAt: toIso(row.onboarding_completed_at) } : {},
|
|
239356
|
+
// 新手指引当前步骤(2026-09-08):空 = 未记,前端按各条流的兜底回落。
|
|
239357
|
+
...row.onboarding_step != null ? { onboardingStep: row.onboarding_step } : {}
|
|
238855
239358
|
});
|
|
238856
239359
|
rowToInvitation = (row) => ({
|
|
238857
239360
|
id: row.id,
|
|
@@ -238863,6 +239366,15 @@ var init_postgres_control_plane = __esm({
|
|
|
238863
239366
|
// 存量邀请行为 NULL → 不带该键,读侧降级为不显示邀请人(不编名字)。
|
|
238864
239367
|
...row.invited_by != null ? { invitedBy: row.invited_by } : {}
|
|
238865
239368
|
});
|
|
239369
|
+
rowToInviteLink = (row) => ({
|
|
239370
|
+
companyId: row.company_id,
|
|
239371
|
+
token: row.token,
|
|
239372
|
+
role: row.role,
|
|
239373
|
+
// NULL → 不带该键,读侧降级为不显示邀请人(与 rowToInvitation 的 invited_by 同一约定)。
|
|
239374
|
+
...row.created_by != null ? { createdBy: row.created_by } : {},
|
|
239375
|
+
createdAt: toIso(row.created_at),
|
|
239376
|
+
expiresAt: toIso(row.expires_at)
|
|
239377
|
+
});
|
|
238866
239378
|
rowToAccount = (row) => ({
|
|
238867
239379
|
id: row.id,
|
|
238868
239380
|
email: row.email,
|
|
@@ -252993,7 +253505,7 @@ function apiUrlFromGatewayUrl(raw) {
|
|
|
252993
253505
|
return void 0;
|
|
252994
253506
|
}
|
|
252995
253507
|
}
|
|
252996
|
-
var NODE_DAEMON_PKG =
|
|
253508
|
+
var NODE_DAEMON_PKG = DAEMON_NPM_PACKAGE;
|
|
252997
253509
|
var NPM_LATEST_TTL_MS = 5 * 6e4;
|
|
252998
253510
|
var npmLatestCache = null;
|
|
252999
253511
|
async function fetchLatestNpmDaemonVersion() {
|
|
@@ -259559,6 +260071,7 @@ var DaemonWsClient = class {
|
|
|
259559
260071
|
this.preflight = preflight;
|
|
259560
260072
|
this.onUpdate = onUpdate;
|
|
259561
260073
|
this.daemonVersion = daemonVersion;
|
|
260074
|
+
this.daemonPkg = opts.daemonPkg;
|
|
259562
260075
|
this.spawnSessionProcess = opts.spawnSessionProcess;
|
|
259563
260076
|
this.countDetachedSessions = opts.countDetachedSessions;
|
|
259564
260077
|
this.pingIntervalMs = opts.pingIntervalMs ?? PING_INTERVAL_MS;
|
|
@@ -259574,7 +260087,10 @@ var DaemonWsClient = class {
|
|
|
259574
260087
|
pingTimer = null;
|
|
259575
260088
|
stopped = false;
|
|
259576
260089
|
/** 收到 update 但本进程内还有活跃会话时置真:延后到全部结束再自更新,避免打断执行中的 agent。 */
|
|
260090
|
+
daemonPkg;
|
|
259577
260091
|
pendingUpdate = false;
|
|
260092
|
+
/** 延后的那次 update 帧带的目标包名(ADR 0515);延后期间必须记住,否则补跑时会退回本包升版本。 */
|
|
260093
|
+
pendingUpdatePkg;
|
|
259578
260094
|
/** 第一次延后这次更新的时刻。用于给「等空闲」封顶——否则长跑会话能把更新无限期挂住(ADR 0115 缺陷 3)。 */
|
|
259579
260095
|
pendingUpdateSince = null;
|
|
259580
260096
|
/** 「等空闲」的上限;到点就照做(会打断本进程内的会话)。env OASIS_UPDATE_DEFER_MAX_MS 可调。 */
|
|
@@ -259902,6 +260418,7 @@ var DaemonWsClient = class {
|
|
|
259902
260418
|
break;
|
|
259903
260419
|
}
|
|
259904
260420
|
case "update": {
|
|
260421
|
+
this.pendingUpdatePkg = msg.pkg;
|
|
259905
260422
|
if (!this.onUpdate) {
|
|
259906
260423
|
log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u672A\u6CE8\u5165\u81EA\u66F4\u65B0\u903B\u8F91\uFF08\u975E node-daemon \u6258\u7BA1\uFF1F\uFF09");
|
|
259907
260424
|
break;
|
|
@@ -259917,11 +260434,11 @@ var DaemonWsClient = class {
|
|
|
259917
260434
|
log2("[node-cli]", `\u2190 update\uFF1A\u5DF2\u5EF6\u540E ${Math.round((Date.now() - (this.pendingUpdateSince ?? Date.now())) / 6e4)} \u5206\u949F\u4ECD\u672A\u7A7A\u95F2\uFF0C\u8D85\u8FC7\u4E0A\u9650\uFF0C\u73B0\u5728\u5C31\u66F4\u65B0\uFF08\u4F1A\u6253\u65AD ${active} \u4E2A\u672C\u8FDB\u7A0B\u5185\u4F1A\u8BDD\uFF09`);
|
|
259918
260435
|
this.pendingUpdate = false;
|
|
259919
260436
|
this.pendingUpdateSince = null;
|
|
259920
|
-
this.onUpdate();
|
|
260437
|
+
this.onUpdate(this.pendingUpdatePkg);
|
|
259921
260438
|
} else {
|
|
259922
260439
|
const detached = this.countDetachedSessions?.() ?? 0;
|
|
259923
260440
|
log2("[node-cli]", detached > 0 ? `\u2190 update\uFF1A\u672C\u8FDB\u7A0B\u5185\u65E0\u4F1A\u8BDD\uFF08\u53E6\u6709 ${detached} \u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u5728\u8DD1\uFF0C**\u4E0D\u53D7\u672C\u6B21\u91CD\u542F\u5F71\u54CD**\uFF09\uFF0C\u7ACB\u5373\u62C9\u53D6\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F` : "\u2190 update\uFF1A\u7A7A\u95F2,\u7ACB\u5373\u62C9\u53D6\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F");
|
|
259924
|
-
this.onUpdate();
|
|
260441
|
+
this.onUpdate(this.pendingUpdatePkg);
|
|
259925
260442
|
}
|
|
259926
260443
|
break;
|
|
259927
260444
|
}
|
|
@@ -260014,7 +260531,7 @@ var DaemonWsClient = class {
|
|
|
260014
260531
|
this.pendingUpdate = false;
|
|
260015
260532
|
this.pendingUpdateSince = null;
|
|
260016
260533
|
log2("[node-cli]", "\u6240\u6709\u4F1A\u8BDD\u5DF2\u7ED3\u675F,\u6267\u884C\u5EF6\u540E\u7684\u66F4\u65B0\uFF1A\u62C9\u53D6\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F");
|
|
260017
|
-
this.onUpdate?.();
|
|
260534
|
+
this.onUpdate?.(this.pendingUpdatePkg);
|
|
260018
260535
|
}
|
|
260019
260536
|
}
|
|
260020
260537
|
buildMeta() {
|
|
@@ -260029,6 +260546,7 @@ var DaemonWsClient = class {
|
|
|
260029
260546
|
runtimeCapabilities: this.runtimes,
|
|
260030
260547
|
nodeVersion: process.version,
|
|
260031
260548
|
...this.daemonVersion ? { daemonVersion: this.daemonVersion } : {},
|
|
260549
|
+
...this.daemonPkg ? { daemonPkg: this.daemonPkg } : {},
|
|
260032
260550
|
connectorReadiness: inspectConnectorRuntimeReadiness(),
|
|
260033
260551
|
...this.nodeName ? { nodeName: this.nodeName } : {},
|
|
260034
260552
|
// ADR-0147:容器执行能力(podman 探测,进程内缓存)。带 executionEnv 的 job 只该派给 true 的节点。
|
|
@@ -260182,6 +260700,7 @@ async function startNode(opts) {
|
|
|
260182
260700
|
}
|
|
260183
260701
|
const sessions = new SessionManager(buildRoutedAdapter());
|
|
260184
260702
|
const client = new DaemonWsClient(opts.serverUrl, opts.nodeId, sessions, adapters, opts.token, runtimes, opts.nodeName, opts.reportRuntimes ?? false, true, opts.onUpdate, opts.daemonVersion, {
|
|
260703
|
+
...opts.daemonPkg ? { daemonPkg: opts.daemonPkg } : {},
|
|
260185
260704
|
// chat 文件浏览 handler 用它定位会话目录;production 不传 = 与 adapter 同款默认根(~/.oasis/work)。
|
|
260186
260705
|
...opts.workRoot !== void 0 ? { workRoot: opts.workRoot } : {},
|
|
260187
260706
|
...opts.spawnSessionProcess ? { spawnSessionProcess: opts.spawnSessionProcess } : {},
|
|
@@ -265368,7 +265887,8 @@ var home = () => os13.homedir();
|
|
|
265368
265887
|
var oasisDir = () => path38.join(home(), ".oasis");
|
|
265369
265888
|
var versionsDir = () => path38.join(oasisDir(), "versions");
|
|
265370
265889
|
var versionDir = (label) => path38.join(versionsDir(), label);
|
|
265371
|
-
var
|
|
265890
|
+
var LEGACY_PKG_NAME = "oasis_test";
|
|
265891
|
+
var npmPrefix = (pkg = LEGACY_PKG_NAME) => pkg === LEGACY_PKG_NAME ? path38.join(oasisDir(), "npm-global") : path38.join(oasisDir(), "npm-global", pkg);
|
|
265372
265892
|
var localBin = () => path38.join(home(), ".local", "bin", "oasis");
|
|
265373
265893
|
var instancesRoot = () => path38.join(oasisDir(), "instances");
|
|
265374
265894
|
function isValidInstanceName(name) {
|
|
@@ -265438,17 +265958,18 @@ var LEGACY_LAUNCHD_LABEL = "com.oasis.node";
|
|
|
265438
265958
|
var import_node_crypto80 = require("node:crypto");
|
|
265439
265959
|
var fs42 = __toESM(require("node:fs"));
|
|
265440
265960
|
var path39 = __toESM(require("node:path"));
|
|
265441
|
-
function versionLabel(sourceFile, pkgVersion, npmPrefixDir) {
|
|
265442
|
-
|
|
265961
|
+
function versionLabel(sourceFile, pkgVersion, npmPrefixDir, pkgName = LEGACY_PKG_NAME) {
|
|
265962
|
+
const tag = pkgName === LEGACY_PKG_NAME ? "" : `+pkg.${pkgName.replace(/[^A-Za-z0-9-]/g, "-")}`;
|
|
265963
|
+
if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return `${pkgVersion}${tag}`;
|
|
265443
265964
|
const digest = (0, import_node_crypto80.createHash)("sha256").update(fs42.readFileSync(sourceFile)).digest("hex").slice(0, 8);
|
|
265444
|
-
return `${pkgVersion}+local.${digest}`;
|
|
265965
|
+
return tag ? `${pkgVersion}${tag}.local.${digest}` : `${pkgVersion}+local.${digest}`;
|
|
265445
265966
|
}
|
|
265446
265967
|
function isFromNpmPrefix(sourceFile, npmPrefixDir) {
|
|
265447
265968
|
const src = realpathOrSelf(sourceFile);
|
|
265448
265969
|
const root = realpathOrSelf(path39.join(npmPrefixDir, "lib", "node_modules"));
|
|
265449
265970
|
return src.startsWith(root + path39.sep);
|
|
265450
265971
|
}
|
|
265451
|
-
var isLocalLabel = (label) =>
|
|
265972
|
+
var isLocalLabel = (label) => /(?:^|[+.])local\.[0-9a-f]{8}$/.test(label);
|
|
265452
265973
|
function installBinaryAtomic(sourceFile, targetPath) {
|
|
265453
265974
|
if (realpathOrSelf(sourceFile) === realpathOrSelf(targetPath)) return;
|
|
265454
265975
|
fs42.mkdirSync(path39.dirname(targetPath), { recursive: true });
|
|
@@ -265563,7 +266084,7 @@ function shimScript() {
|
|
|
265563
266084
|
}
|
|
265564
266085
|
|
|
265565
266086
|
// src/index.ts
|
|
265566
|
-
var PKG_VERSION = true ? "0.
|
|
266087
|
+
var PKG_VERSION = true ? "2.0.0" : "dev";
|
|
265567
266088
|
var LOCAL_BIN = localBin();
|
|
265568
266089
|
var NPM_PREFIX = npmPrefix();
|
|
265569
266090
|
var INSTANCE = DEFAULT_INSTANCE;
|
|
@@ -265581,8 +266102,14 @@ function useInstance(name) {
|
|
|
265581
266102
|
LOG_FILE = INST.logFile;
|
|
265582
266103
|
BIN_DIR = INST.binDir;
|
|
265583
266104
|
BIN_FILE = INST.binFile;
|
|
266105
|
+
usePkgName(readConfigAt(INST.configFile)?.pkgName ?? BUILT_PKG_NAME);
|
|
266106
|
+
}
|
|
266107
|
+
var BUILT_PKG_NAME = true ? "oasis_test_v2" : LEGACY_PKG_NAME2;
|
|
266108
|
+
var PKG_NAME = BUILT_PKG_NAME;
|
|
266109
|
+
function usePkgName(pkg) {
|
|
266110
|
+
PKG_NAME = pkg;
|
|
266111
|
+
NPM_PREFIX = npmPrefix(pkg);
|
|
265584
266112
|
}
|
|
265585
|
-
var PKG_NAME = true ? "oasis_test_v2" : "oasis_test";
|
|
265586
266113
|
var parseFlags = (argv) => {
|
|
265587
266114
|
const flags = /* @__PURE__ */ new Map();
|
|
265588
266115
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -265770,9 +266297,28 @@ function stopDaemon(name) {
|
|
|
265770
266297
|
}
|
|
265771
266298
|
}
|
|
265772
266299
|
}
|
|
265773
|
-
|
|
266300
|
+
var PKG_SWITCH_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
266301
|
+
function pkgToSwitchTo(serverPkg) {
|
|
266302
|
+
const pkg = (serverPkg ?? "").trim();
|
|
266303
|
+
if (!pkg || pkg === PKG_NAME) return void 0;
|
|
266304
|
+
const last = readConfig()?.pkgSwitchedAt ?? 0;
|
|
266305
|
+
const sinceMs = Date.now() - last;
|
|
266306
|
+
if (last && sinceMs < PKG_SWITCH_COOLDOWN_MS) {
|
|
266307
|
+
console.error(
|
|
266308
|
+
`[oasis] \u5FFD\u7565\u6362\u5305 ${PKG_NAME} \u2192 ${pkg}\uFF1A${Math.round(sinceMs / 1e3)}s \u524D\u521A\u6362\u8FC7\u4E00\u6B21\u3002\u4E24\u4E2A\u63A7\u5236\u9762\u5BF9\u672C\u8282\u70B9\u671F\u671B\u7684\u5305\u540D\u4E0D\u4E00\u81F4\u65F6\u4F1A\u65E0\u9650\u4E92\u5207\uFF0C\u5148\u53BB\u6838\u5BF9\u914D\u7F6E\u3002`
|
|
266309
|
+
);
|
|
266310
|
+
return void 0;
|
|
266311
|
+
}
|
|
266312
|
+
return pkg;
|
|
266313
|
+
}
|
|
266314
|
+
function selfUpdate(targetPkg) {
|
|
265774
266315
|
const cfg = readConfig();
|
|
265775
266316
|
if (!cfg?.serverUrl) throw new Error("no config \u2014 run `oasis daemon start` first");
|
|
266317
|
+
if (targetPkg && targetPkg !== PKG_NAME) {
|
|
266318
|
+
console.error(`[oasis] \u6362\u901A\u9053\uFF1A${PKG_NAME} \u2192 ${targetPkg}\uFF08\u672C\u63A7\u5236\u9762\u671F\u671B\u7684\u5305\uFF09`);
|
|
266319
|
+
saveConfig({ ...cfg, pkgName: targetPkg, pkgSwitchedAt: Date.now() });
|
|
266320
|
+
usePkgName(targetPkg);
|
|
266321
|
+
}
|
|
265776
266322
|
const nameArg = cfg.name ? ` --name '${cfg.name.replace(/'/g, "'\\''")}'` : "";
|
|
265777
266323
|
const oasisBin = path40.join(NPM_PREFIX, "bin", "oasis");
|
|
265778
266324
|
const startCmd = `"${oasisBin}" daemon start --upgrade --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
|
|
@@ -265872,17 +266418,28 @@ async function startOneInstance(o) {
|
|
|
265872
266418
|
token = prev.token;
|
|
265873
266419
|
}
|
|
265874
266420
|
if (!token || !nodeId) {
|
|
265875
|
-
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
|
|
266421
|
+
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--pkg <npm \u5305\u540D>] [--upgrade]");
|
|
265876
266422
|
process.exit(1);
|
|
265877
266423
|
}
|
|
265878
266424
|
const name = o.applyName ? flags.get("name") ?? prev?.name : prev?.name;
|
|
265879
266425
|
const nameChanged = prev?.name !== void 0 && name !== prev.name;
|
|
265880
266426
|
const savedCompanyId = companyId?.trim() || prev?.companyId?.trim();
|
|
265881
|
-
|
|
266427
|
+
const savedPkgName = (flags.get("pkg") || "").trim() || prev?.pkgName;
|
|
266428
|
+
if (savedPkgName) usePkgName(savedPkgName);
|
|
266429
|
+
saveConfig({
|
|
266430
|
+
serverUrl,
|
|
266431
|
+
token,
|
|
266432
|
+
nodeId,
|
|
266433
|
+
...name ? { name } : {},
|
|
266434
|
+
...savedCompanyId ? { companyId: savedCompanyId } : {},
|
|
266435
|
+
...savedPkgName ? { pkgName: savedPkgName } : {},
|
|
266436
|
+
...prev?.pkgSwitchedAt ? { pkgSwitchedAt: prev.pkgSwitchedAt } : {},
|
|
266437
|
+
reportRuntimesOnNextStart: true
|
|
266438
|
+
});
|
|
265882
266439
|
const before = currentLabelOf(INST);
|
|
265883
266440
|
let label = before;
|
|
265884
266441
|
if (!instanceHasSoftware(INST) || upgrade) {
|
|
265885
|
-
const next = versionLabel(process.argv[1], PKG_VERSION, NPM_PREFIX);
|
|
266442
|
+
const next = versionLabel(process.argv[1], PKG_VERSION, NPM_PREFIX, PKG_NAME);
|
|
265886
266443
|
if (before && isLocalLabel(before) && before !== next) {
|
|
265887
266444
|
console.log(` \u26A0 \u5373\u5C06\u7528 ${next} \u8986\u76D6\u672C\u5B9E\u4F8B\u6B63\u5728\u8DD1\u7684\u672C\u5730\u6784\u5EFA ${before}`);
|
|
265888
266445
|
}
|
|
@@ -265940,7 +266497,8 @@ function spawnSessionProcess(spec) {
|
|
|
265940
266497
|
...spec,
|
|
265941
266498
|
sessionDir: dir,
|
|
265942
266499
|
nodeId: cfg?.nodeId ?? INSTANCE,
|
|
265943
|
-
daemonVersion: PKG_VERSION
|
|
266500
|
+
daemonVersion: PKG_VERSION,
|
|
266501
|
+
daemonPkg: PKG_NAME
|
|
265944
266502
|
}), { mode: 384 });
|
|
265945
266503
|
let pinnedDir = INST.currentLink;
|
|
265946
266504
|
try {
|
|
@@ -266077,9 +266635,10 @@ void (async () => {
|
|
|
266077
266635
|
nodeName: cfg.name ?? cfg.nodeId,
|
|
266078
266636
|
reportRuntimes,
|
|
266079
266637
|
daemonVersion: PKG_VERSION,
|
|
266080
|
-
|
|
266638
|
+
daemonPkg: PKG_NAME,
|
|
266639
|
+
onUpdate: (pkg) => {
|
|
266081
266640
|
try {
|
|
266082
|
-
selfUpdate();
|
|
266641
|
+
selfUpdate(pkgToSwitchTo(pkg));
|
|
266083
266642
|
} catch (e) {
|
|
266084
266643
|
console.error("[oasis] self-update failed:", String(e));
|
|
266085
266644
|
}
|
|
@@ -266142,12 +266701,14 @@ void (async () => {
|
|
|
266142
266701
|
process.exit(1);
|
|
266143
266702
|
}
|
|
266144
266703
|
const cur = currentLabelOf(INST);
|
|
266704
|
+
const wantPkg = (flags.get("pkg") || "").trim();
|
|
266145
266705
|
if (cur && isLocalLabel(cur)) {
|
|
266146
|
-
console.log(` \u26A0 \u672C\u5B9E\u4F8B\u73B0\u5728\u8DD1\u7684\u662F\u672C\u5730\u6784\u5EFA ${cur}\uFF0C\u672C\u6B21\u66F4\u65B0\u4F1A\u7528 ${PKG_NAME}@latest \u8986\u76D6\u5B83\u3002`);
|
|
266706
|
+
console.log(` \u26A0 \u672C\u5B9E\u4F8B\u73B0\u5728\u8DD1\u7684\u662F\u672C\u5730\u6784\u5EFA ${cur}\uFF0C\u672C\u6B21\u66F4\u65B0\u4F1A\u7528 ${wantPkg || PKG_NAME}@latest \u8986\u76D6\u5B83\u3002`);
|
|
266147
266707
|
}
|
|
266148
|
-
console.log(`\u2192 \
|
|
266149
|
-
|
|
266150
|
-
|
|
266708
|
+
if (wantPkg && wantPkg !== PKG_NAME) console.log(`\u2192 \u6362\u901A\u9053\uFF1A${PKG_NAME} \u2192 ${wantPkg}`);
|
|
266709
|
+
console.log(`\u2192 \u66F4\u65B0 ${wantPkg || PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
|
|
266710
|
+
selfUpdate(wantPkg || void 0);
|
|
266711
|
+
console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${wantPkg || PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
|
|
266151
266712
|
return;
|
|
266152
266713
|
}
|
|
266153
266714
|
if (dsub === "status") {
|