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.
Files changed (2) hide show
  1. package/dist/index.js +649 -88
  2. 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
- source text NOT NULL CHECK (source IN ('type','manual','model')),
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
- -- \u5148\u68C0\u67E5\u5168\u90E8\u65E7\u5217\uFF0C\u518D\u5220\u5217\uFF1B\u9047\u5230\u975E\u7A7A\u5386\u53F2\u503C\u5C31\u505C\u6B62\uFF0C\u7981\u6B62\u731C\u6D4B\u6839\u8282\u70B9\u5E76\u8986\u76D6\u771F\u5B9E\u6765\u6E90\u3002
9518
- -- \u4E0D\u7528 CASCADE\uFF1A\u4ED3\u5E93\u5916\u7684\u4F9D\u8D56\u89C6\u56FE\u5FC5\u987B\u663E\u5F0F\u5904\u7406\u3002DDL \u5355\u6B21 query \u5185\u539F\u5B50\u56DE\u6EDA\u3002
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 EXCEPTION 'workorders.% contains legacy data; reconcile and archive it before migrating (ADR 0308)', column_name_to_drop;
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
- -- \u4FDD\u7559\u4E00\u4EFD\u89C6\u56FE\u65E2\u6CA1\u4EBA\u8BFB\u3001\u53E3\u5F84\u53C8\u5BF9\u4E0D\u4E0A\uFF0C\u53EA\u4F1A\u662F\u7B2C\u4E09\u5904\u5BF9\u4E0D\u4E0A\u7684\u8FDB\u5EA6\u6570\u3002\u8FD9\u91CC\u4E0D\u518D CREATE\uFF1B\u4E0B\u9762\u7684 DROP \u8BA9\u5B58\u91CF\u5E93
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
- DROP VIEW IF EXISTS ${q2}.workorder_view;
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: randomBytes12,
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 randomBytes12(length) {
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) => ({ actor: r.reviewerActorId, source: r.source })) : void 0;
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 manual = (a.reviewers ?? []).map((r) => ({ actor: r.actor, source: r.source }));
19129
- const gateActors = humanGateActorsOf(this.model, artifactId);
19130
- for (const actor of gateActors) {
19131
- if (!manual.some((r) => r.actor === actor)) manual.push({ actor, source: "manual" });
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 typeBuckets = [];
19135
- for (const gate of typeDef?.reviews?.gates ?? []) {
19136
- for (const role of gate.reviewers) {
19137
- typeBuckets.push({ source: "type", eligible: [], quorum: 1, roles: [role] });
19138
- }
19139
- }
19140
- for (const role of a.reviewRoles ?? []) {
19141
- if (typeBuckets.some((b2) => b2.roles[0] === role)) continue;
19142
- typeBuckets.push({ source: "type", eligible: [], quorum: 1, roles: [role] });
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: manual,
19146
- quorum: manual.length + typeBuckets.length,
19147
- requirements: [...typeBuckets, ...manual.length > 0 ? [{ source: "manual", eligible: manual.map((r) => r.actor), quorum: manual.length }] : []],
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: randomBytes12, createHash: createHash37 } = require("crypto");
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 = randomBytes12(16).toString("base64");
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
- message: `${a.id} \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`
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.id} \u7684\u90E8\u4EF6 ${p2.name} \u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216\u6362 role\u3002`
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
- message: `${s2.id} \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`
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) => ({ id: op.id, type: op.type, ...op.owner !== void 0 ? { owner: op.owner } : {} })),
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 ? "\u8BE5\u90E8\u7F72\u6CA1\u63A5 enqueuePending\uFF08\u6700\u5C0F\u88C5\u914D / \u5355\u6D4B\uFF09\uFF0C\u975E\u751F\u4EA7\u4E8B\u6545\uFF1B\u751F\u4EA7\u4E0A\u5FC5\u987B\u63A5\u961F\u5217" : 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";
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
- onboardingCompletedAt: this.now()
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 Date.parse(inv.expiresAt) <= Date.parse(this.now());
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
- router.post("/api/invitations/:invitationId/accept", async (req) => {
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
- const member = await service.commitAcceptedInvitation(inv, me).catch(mapErr);
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
- return `npm install -g --prefer-online --prefix ~/.oasis/npm-global oasis_test@latest && ~/.oasis/npm-global/bin/oasis daemon start --instance ${shellQuote(instanceName)} --server-ws ${shellQuote(p2.gatewayUrl)}${companyPart}${namePart} --enroll-token ${shellQuote(p2.token)}`;
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
- if (latest && current && !daemonVersionLess(current, latest)) {
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
- ...body2 ? { detail: body2 } : {},
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: "open_issue", label: BUTTON.handle, target: issueId }]
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
- // 空 = 还没走完被邀请者三步向导(= 不算真正加入)。timestamptz → ISO 串,与其它时间字段一致。
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 = "oasis_test";
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 npmPrefix = () => path38.join(oasisDir(), "npm-global");
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
- if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return pkgVersion;
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) => label.includes("+local.");
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.2.1" : "dev";
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
- function selfUpdate() {
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
- saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, ...savedCompanyId ? { companyId: savedCompanyId } : {}, reportRuntimesOnNextStart: true });
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
- onUpdate: () => {
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 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
266149
- selfUpdate();
266150
- console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
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") {