oasis_test_v2 2.2.12 → 2.2.14

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 +213 -25
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -201815,6 +201815,15 @@ var init_provision = __esm({
201815
201815
  });
201816
201816
 
201817
201817
  // ../connectors/src/prepare-job.ts
201818
+ function loginShellShadow(commandName) {
201819
+ const home2 = process.env["HOME"];
201820
+ if (!home2) return "";
201821
+ for (const dir of [(0, import_node_path17.join)(home2, ".local", "bin"), (0, import_node_path17.join)(home2, "bin")]) {
201822
+ const candidate = (0, import_node_path17.join)(dir, commandName);
201823
+ if ((0, import_node_fs11.existsSync)(candidate)) return candidate;
201824
+ }
201825
+ return "";
201826
+ }
201818
201827
  async function prepareConnectorsForJob(job, deps) {
201819
201828
  const creds = job.connectorCreds ?? [];
201820
201829
  const remoteWrappers = job.wrapperPaths ?? [];
@@ -201850,7 +201859,16 @@ async function prepareConnectorsForJob(job, deps) {
201850
201859
  localWrappers.push(staged);
201851
201860
  status.wrapperReady = true;
201852
201861
  }
201853
- log("[node-connectors]", `${slug6}: creds + wrapper \u5DF2\u843D\u5728\u672C\u673A`);
201862
+ const shadow = loginShellShadow(connector.commandName);
201863
+ if (shadow) {
201864
+ status.shadowedBy = shadow;
201865
+ log(
201866
+ "[node-connectors]",
201867
+ `\u26A0 ${slug6}: creds + wrapper \u5DF2\u843D\u5728\u672C\u673A\uFF0C\u4F46 **${shadow} \u4F1A\u5728\u767B\u5F55 shell \u91CC\u6321\u4F4F wrapper**\u2014\u2014Ubuntu \u7684 ~/.profile \u628A $HOME/.local/bin \u524D\u7F6E\u5230 PATH \u6700\u524D\u3002agent \u4E00\u65E6\u7528 \`bash -lc\` \u8C03 ${connector.commandName}\uFF0C\u8D70\u5230\u7684\u662F\u90A3\u4E2A\u5BBF\u4E3B\u526F\u672C\u3001\u914D\u7F6E\u76EE\u5F55\u53C8\u88AB\u9694\u79BB\u6210\u7A7A\u7684\uFF0C\u7ED3\u679C\u5C31\u662F\u300C\u672A\u914D\u7F6E\u300D\u3002\u5904\u7406\u529E\u6CD5\uFF1A\u5220\u6389 ${shadow}\uFF08\u8FDE\u63A5\u5668\u81EA\u5E26\u7684\u771F CLI \u5728 ~/.oasis/connector-tools/bin \u4E0B\uFF0C\u4E0D\u53D7\u5F71\u54CD\uFF09\u3002`
201868
+ );
201869
+ } else {
201870
+ log("[node-connectors]", `${slug6}: creds + wrapper \u5DF2\u843D\u5728\u672C\u673A`);
201871
+ }
201854
201872
  } catch (err) {
201855
201873
  status.error = String(err);
201856
201874
  log(
@@ -203664,6 +203682,14 @@ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, para
203664
203682
  throw new Error(`\u672A\u77E5\u89C6\u56FE: ${name}`);
203665
203683
  }
203666
203684
  }
203685
+ function resolveListenTarget(env, pid, port) {
203686
+ const count2 = Number(env["LISTEN_FDS"] ?? "0");
203687
+ const listenPid = env["LISTEN_PID"];
203688
+ if (Number.isFinite(count2) && count2 >= 1 && (!listenPid || listenPid === String(pid))) {
203689
+ return { fd: 3 };
203690
+ }
203691
+ return { port: port ?? 0, host: "0.0.0.0" };
203692
+ }
203667
203693
  async function startOasisServer(opts) {
203668
203694
  const resolveActor = opts.resolveActor ?? defaultResolveActor;
203669
203695
  const defaultDraftPlannerIssuesStore = new MemoryWorkorderDraftPlannerIssuesStore();
@@ -206756,6 +206782,52 @@ ${composed}`;
206756
206782
  }
206757
206783
  return;
206758
206784
  }
206785
+ if (url.pathname.startsWith("/api/connectors/") && /\/auth\/(begin|rollback|commit)$/.test(url.pathname) && req.method === "POST") {
206786
+ const m2 = url.pathname.match(/^\/api\/connectors\/([^/]+)\/auth\/(begin|rollback|commit)$/);
206787
+ try {
206788
+ if (!m2) throw new Error("bad path");
206789
+ const connectorId = decodeURIComponent(m2[1]);
206790
+ const op = m2[2];
206791
+ const { actorId } = JSON.parse(rawBody || "{}");
206792
+ if (!actorId) {
206793
+ res.writeHead(400).end(JSON.stringify({ error: "actorId required" }));
206794
+ return;
206795
+ }
206796
+ const svc = await connectorActorsService();
206797
+ if (!svc) throw new Error("actors service unavailable");
206798
+ const cacheKey = `${currentCompanyId ?? ""}::${actorId}::${connectorId}`;
206799
+ const now = Date.now();
206800
+ for (const [k2, v2] of connectorAuthSnapshots) {
206801
+ if (now - v2.at > CONNECTOR_AUTH_SNAPSHOT_TTL_MS) connectorAuthSnapshots.delete(k2);
206802
+ }
206803
+ if (op === "begin") {
206804
+ const rows = await svc.snapshotActorConnectorVars(actorId, connectorId);
206805
+ connectorAuthSnapshots.set(cacheKey, { rows, at: now });
206806
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true, snapshot: rows.length }));
206807
+ return;
206808
+ }
206809
+ if (op === "commit") {
206810
+ connectorAuthSnapshots.delete(cacheKey);
206811
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true }));
206812
+ return;
206813
+ }
206814
+ const snap = connectorAuthSnapshots.get(cacheKey);
206815
+ if (!snap) {
206816
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: false, reason: "no_snapshot" }));
206817
+ return;
206818
+ }
206819
+ const out = await svc.restoreActorConnectorVars(
206820
+ actorId,
206821
+ connectorId,
206822
+ snap.rows
206823
+ );
206824
+ connectorAuthSnapshots.delete(cacheKey);
206825
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true, ...out }));
206826
+ } catch (e) {
206827
+ res.writeHead(500).end(JSON.stringify({ error: String(e) }));
206828
+ }
206829
+ return;
206830
+ }
206759
206831
  if (req.method === "POST" && /^\/api\/connectors\/[^/]+\/disconnect$/.test(url.pathname)) {
206760
206832
  const connId = decodeURIComponent(url.pathname.split("/")[3] ?? "");
206761
206833
  const svc = await connectorActorsService();
@@ -207193,7 +207265,7 @@ ${composed}`;
207193
207265
  grantedFrom = "installation";
207194
207266
  }
207195
207267
  }
207196
- const missing = missingAppPermissions(effective);
207268
+ const missing = missingAppPermissions({ ...meta.permissions, ...effective });
207197
207269
  const requestedAll = Object.entries(meta.permissions).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key));
207198
207270
  const grantedKeys = new Set(
207199
207271
  Object.entries(effective).filter(([, level]) => Boolean(level)).map(([k2]) => k2)
@@ -207218,13 +207290,19 @@ ${composed}`;
207218
207290
  */
207219
207291
  otherInstallations: installs.filter((i) => i.installationId !== installationId).map((i) => ({ account: i.account, count: Object.keys(i.permissions).length })),
207220
207292
  /**
207221
- * App 申请了、但**这一处安装铸出来的 token 里没有**。
207222
- * **在全集上做差**,不再只看本仓必需的那八项(否则人另外勾的永远报不出来)。
207223
- *
207224
- * ⚠ 差集只说明「这一处安装现在给不了」,不等于「人没批」——同一个 App 装多处时
207225
- * 批准是逐处的(见 `otherInstallations`)。界面别把这层推断说死。
207293
+ * 保留只为**兼容老前端**(新卡片不再读它):新口径不分「已授权 / 申请中」,
207294
+ * 见 `detected`。这一格哪天确认没有旧客户端在读了就可以删。
207226
207295
  */
207227
207296
  awaitingApproval: requestedAll.filter((r) => !grantedKeys.has(r.key)).map((r) => r.key),
207297
+ /**
207298
+ * **检测到的全部权限**(App 申请的 ∪ 真拿得到的),英文原名 + 级别,按名排序。
207299
+ * 界面就照这一份平铺成一排 chip,不分组、不计数、不解释审批状态。
207300
+ */
207301
+ detected: [.../* @__PURE__ */ new Set([...requestedAll.map((r) => r.key), ...grantedKeys])].sort((a, b2) => a.localeCompare(b2)).map((key) => ({
207302
+ key,
207303
+ // 真给的级别优先;只在 App 侧见过的才回落成它申请的级别。
207304
+ level: String(effective[key] ?? meta.permissions[key] ?? "")
207305
+ })),
207228
207306
  /**
207229
207307
  * agent **真正拿得到**的全部权限(优先读 token 自己的权限位),一律用 GitHub 的英文原名 + 级别
207230
207308
  * (2026-09-10 发起人:「所有权限都用英文原文,不区分是不是八项权限」)。
@@ -207630,7 +207708,7 @@ ${composed}`;
207630
207708
  });
207631
207709
  httpServer.keepAliveTimeout = 65e3;
207632
207710
  httpServer.headersTimeout = 66e3;
207633
- await new Promise((resolve10) => httpServer.listen(opts.port ?? 0, "0.0.0.0", resolve10));
207711
+ await new Promise((resolve10) => httpServer.listen(resolveListenTarget(process.env, process.pid, opts.port), resolve10));
207634
207712
  const address = httpServer.address();
207635
207713
  if (address === null || typeof address === "string") throw new Error("listen failed");
207636
207714
  const baseUrl = `http://127.0.0.1:${address.port}`;
@@ -207649,19 +207727,46 @@ ${composed}`;
207649
207727
  * 委派域没装配时返回 null,调用方照常。
207650
207728
  */
207651
207729
  settleDelegationFromSweep: (childSessionId, reason, companyId, opts2) => delegationService?.settleFromSweep(childSessionId, reason, companyId, opts2) ?? Promise.resolve(null),
207652
- close: async () => {
207730
+ /**
207731
+ * @param opts.drainMs **优雅停机预算**(毫秒)。缺省 0 = 老行为:立刻掐断所有连接后关闭。
207732
+ *
207733
+ * 给了预算就改成「排空」:先 `close()` 停收新连接、`closeIdleConnections()` 放掉空闲的
207734
+ * keep-alive,**在途请求让它跑完**;预算到点还没走完的(SSE 这类长连接本来就不会自己结束)
207735
+ * 再 `closeAllConnections()` 强断。前端的流有重连(`hydrateAndAttach`),被强断能自愈;
207736
+ * 普通请求跑到一半被掐则是实打实的失败,所以要给它们这几百毫秒。
207737
+ *
207738
+ * 生产上这条由 `oasis serve` 的 SIGTERM 处理器传(见 cli.ts)——现在 systemd 是硬杀
207739
+ * (journal: `code=exited, status=143`),在途请求当场断。
207740
+ */
207741
+ close: async (opts2) => {
207653
207742
  if (broadcastTimer) clearInterval(broadcastTimer);
207654
207743
  if (pendingSystemNoticesRecoveryTimer) clearInterval(pendingSystemNoticesRecoveryTimer);
207655
207744
  await knowledgeDomain?.close();
207656
207745
  channelWsSupervisor?.stop();
207746
+ const drainMs = opts2?.drainMs ?? 0;
207657
207747
  await new Promise((resolve10, reject) => {
207658
- httpServer.closeAllConnections();
207659
- httpServer.close((err) => err ? reject(err) : resolve10());
207748
+ let sweep;
207749
+ let force;
207750
+ httpServer.close((err) => {
207751
+ if (sweep) clearInterval(sweep);
207752
+ if (force) clearTimeout(force);
207753
+ if (err) reject(err);
207754
+ else resolve10();
207755
+ });
207756
+ if (drainMs > 0) {
207757
+ httpServer.closeIdleConnections();
207758
+ sweep = setInterval(() => httpServer.closeIdleConnections(), 100);
207759
+ sweep.unref?.();
207760
+ force = setTimeout(() => httpServer.closeAllConnections(), drainMs);
207761
+ force.unref?.();
207762
+ } else {
207763
+ httpServer.closeAllConnections();
207764
+ }
207660
207765
  });
207661
207766
  }
207662
207767
  };
207663
207768
  }
207664
- var http, import_node_crypto37, githubAppPending, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, STREAMING_PATHS, PLANNER_BLOCKING_CODES, WILL_VERBS, GOVERNANCE_COMMANDS, READ_ONLY_COMMANDS, isAgent, isHuman;
207769
+ var http, import_node_crypto37, githubAppPending, connectorAuthSnapshots, CONNECTOR_AUTH_SNAPSHOT_TTL_MS, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, STREAMING_PATHS, PLANNER_BLOCKING_CODES, WILL_VERBS, GOVERNANCE_COMMANDS, READ_ONLY_COMMANDS, isAgent, isHuman;
207665
207770
  var init_server3 = __esm({
207666
207771
  "../server/src/server.ts"() {
207667
207772
  "use strict";
@@ -207719,6 +207824,8 @@ var init_server3 = __esm({
207719
207824
  init_src8();
207720
207825
  init_continuation();
207721
207826
  githubAppPending = new PendingAppCreations();
207827
+ connectorAuthSnapshots = /* @__PURE__ */ new Map();
207828
+ CONNECTOR_AUTH_SNAPSHOT_TTL_MS = 30 * 60 * 1e3;
207722
207829
  defaultResolveActor = (token) => token.startsWith("token:") ? token.slice("token:".length) : null;
207723
207830
  enc = (s2) => new TextEncoder().encode(s2);
207724
207831
  CHAT_OUTPUT_LIMIT = 16e3;
@@ -210765,6 +210872,18 @@ var init_projection = __esm({
210765
210872
  function isUncategorizedProjectId(projectId2) {
210766
210873
  return projectId2 === UNCATEGORIZED_PROJECT_ID;
210767
210874
  }
210875
+ function latestUncategorizedActivityAt(candidates) {
210876
+ let bestIso = UNCATEGORIZED_UNKNOWN_TIME;
210877
+ let bestMs = Date.parse(UNCATEGORIZED_UNKNOWN_TIME);
210878
+ for (const iso6 of candidates) {
210879
+ if (!iso6) continue;
210880
+ const ms = Date.parse(iso6);
210881
+ if (Number.isNaN(ms) || ms <= bestMs) continue;
210882
+ bestMs = ms;
210883
+ bestIso = iso6;
210884
+ }
210885
+ return bestIso;
210886
+ }
210768
210887
  async function bindWorkorderProject(deps) {
210769
210888
  const { workOrderId, projectId: projectId2 } = deps;
210770
210889
  if (!projectId2) return null;
@@ -211400,6 +211519,10 @@ var init_service4 = __esm({
211400
211519
  async buildUncategorizedProject() {
211401
211520
  const workorders = await this.listProjectWorkorders(UNCATEGORIZED_PROJECT_ID);
211402
211521
  const artifactList = await this.aggregateUncategorizedArtifacts();
211522
+ const updatedAt = latestUncategorizedActivityAt([
211523
+ ...workorders.map((workorder) => workorder.updatedAt),
211524
+ ...artifactList.artifacts.map((artifact) => artifact.updatedAt)
211525
+ ]);
211403
211526
  return {
211404
211527
  id: UNCATEGORIZED_PROJECT_ID,
211405
211528
  name: UNCATEGORIZED_PROJECT_NAME,
@@ -211412,7 +211535,7 @@ var init_service4 = __esm({
211412
211535
  // 出 null 保持「取不到就 null」的域内红线,也不把一个不存在的 actor id 塞进契约。
211413
211536
  createdBy: null,
211414
211537
  createdAt: UNCATEGORIZED_UNKNOWN_TIME,
211415
- updatedAt: UNCATEGORIZED_UNKNOWN_TIME
211538
+ updatedAt
211416
211539
  };
211417
211540
  }
211418
211541
  async aggregateUncategorizedArtifacts() {
@@ -219564,6 +219687,46 @@ ${input.description}
219564
219687
  listActorConnectorConnections(actorId) {
219565
219688
  return this.opts.store.listActorConnectorConnections(actorId);
219566
219689
  }
219690
+ /**
219691
+ * 授权流程**开始前**给这名员工在这个连接器下的全部个人变量拍一张快照(原样密文,不解密)。
219692
+ *
219693
+ * 为什么需要它(2026-09-10 发起人:「如果本身已经有连接器身份,如果重新连接没走完,
219694
+ * 则保留原身份不覆盖,之前重连到一半把原来的身份覆盖了」):
219695
+ * 「重新连接」是**边走边落库**的——飞书在「创建应用」那一步就把 `FEISHU_APP_ID` /
219696
+ * `FEISHU_APP_SECRET` 覆盖掉了,GitHub 在 manifest 回调那一步就把 `GITHUB_APP_*` 覆盖掉了,
219697
+ * 而这两步都在「用户完成授权」**之前**。人中途关掉弹窗,旧身份就已经没了。
219698
+ *
219699
+ * 前端此前的回滚只删「这次新增的键」(基线差集),**对已存在的键无能为力**——它拿不到旧值
219700
+ * (读侧永远只给掩码)。所以快照必须在服务端拍、也在服务端写回。
219701
+ */
219702
+ async snapshotActorConnectorVars(actorId, connectorId) {
219703
+ const rows = await this.opts.store.listVariables(actorId);
219704
+ return rows.filter((v2) => v2.scope === "personal" && v2.actorId === actorId && v2.connectorId === connectorId);
219705
+ }
219706
+ /**
219707
+ * 把快照原样写回:**快照里有的恢复旧值,快照之后新冒出来的键删掉**。
219708
+ *
219709
+ * 密文原样搬运,不经解密再加密——少一次明文落到内存里,也不受密钥轮换影响。
219710
+ * 返回改了几行,调用方好把「回滚做了什么」如实说出来(回滚静默是上一版的毛病)。
219711
+ */
219712
+ async restoreActorConnectorVars(actorId, connectorId, snapshot) {
219713
+ const keep = new Map(snapshot.map((r) => [r.key, r]));
219714
+ const now = await this.snapshotActorConnectorVars(actorId, connectorId);
219715
+ let removed = 0;
219716
+ for (const row of now) {
219717
+ if (keep.has(row.key)) continue;
219718
+ await this.deleteVariable(row.key, actorId);
219719
+ removed += 1;
219720
+ }
219721
+ let restored = 0;
219722
+ for (const row of keep.values()) {
219723
+ const current = now.find((r) => r.key === row.key);
219724
+ if (current && current.valueEncrypted === row.valueEncrypted) continue;
219725
+ await this.opts.store.putVariable({ ...row, updatedAt: this.now() });
219726
+ restored += 1;
219727
+ }
219728
+ return { restored, removed };
219729
+ }
219567
219730
  /**
219568
219731
  * 员工连接器面板所需状态:连接记录 + 组织级已连接集合 + 该员工已授权(有个人 connector 变量)集合。
219569
219732
  * 前端据此算每个 connector 的 hasRecord / 有效 enabled / connected(actor 或 global)。
@@ -223250,8 +223413,12 @@ function toApiProject(project) {
223250
223413
  // - 取不到创建人 → `null`(老的手建项目 / 无分类虚拟卡),前端显示「—」;
223251
223414
  // - 有 id 但名册解析不到 → 对象在、`name`/`avatar` 为 `null`(**不回落 id 尾段**)。
223252
223415
  created_by: project.createdBy ? toApiProjectActorRef(project.createdBy) : null,
223253
- // 空桶哨兵(`UNCATEGORIZED_UNKNOWN_TIME`)→ `null`:无分类项目桶为空时**无可派生时间**,
223254
- // 前端见 null 显示「—」而不是一串远古时间;真项目/非空桶不受影响。
223416
+ // 「时间不可知」哨兵(`UNCATEGORIZED_UNKNOWN_TIME`)→ `null`,前端见 null 显示「—」而不是
223417
+ // 一串远古时间。**只有无分类虚拟卡会命中**,真项目不受影响(建项目时两个字段都写真时间戳)。
223418
+ // 命中的两种情形(2026-09-10 起):
223419
+ // · `created_at`:恒命中——桶不存在「被创建」这回事;
223420
+ // · `updated_at`:只在桶里空无一物时命中;桶里有工单/产物时是真时间(见 service.ts
223421
+ // `buildUncategorizedProject` 与 `latestUncategorizedActivityAt`)。
223255
223422
  created_at: project.createdAt === UNCATEGORIZED_UNKNOWN_TIME ? null : project.createdAt,
223256
223423
  updated_at: project.updatedAt === UNCATEGORIZED_UNKNOWN_TIME ? null : project.updatedAt
223257
223424
  };
@@ -234744,6 +234911,12 @@ function payloadStr(payload, key) {
234744
234911
  }
234745
234912
  return "";
234746
234913
  }
234914
+ function workorderGoal(title, prefix, suffix) {
234915
+ const subject = Array.from(title.replace(/\s+/g, " ").trim());
234916
+ const budget = 50 - Array.from(prefix + suffix).length;
234917
+ const shortTitle = subject.length > budget ? `${subject.slice(0, budget - 1).join("")}\u2026` : subject.join("");
234918
+ return `${prefix}${shortTitle}${suffix}`;
234919
+ }
234747
234920
  function buildPlan(structure, title, ctx, opts = {}) {
234748
234921
  const nodeOwners = payloadNodeOwners(ctx.payload);
234749
234922
  const nodeReviewers = payloadNodeReviewers(ctx.payload);
@@ -234803,9 +234976,7 @@ function bugFixResolve(ctx) {
234803
234976
  artifactType: "diagnosis",
234804
234977
  title,
234805
234978
  brief,
234806
- // goal 用标题兜底:弹窗不再单列「工单目标」(实测无任何消费方读它、让用户填两遍纯属白填),
234807
- // 但字段本身在内核里有设计意图(workorderSpec 读它),故填上而非留空。
234808
- goal: title,
234979
+ goal: workorderGoal(title, "\u4FEE\u590D\u300C", "\u300D\uFF0C\u9A8C\u8BC1\u6240\u62A5\u73B0\u8C61\u4E0D\u518D\u51FA\u73B0\u3002"),
234809
234980
  ...manager ? { manager } : {},
234810
234981
  acceptanceCriteria: ["\u6309\u63CF\u8FF0\u91CC\u7684\u590D\u73B0\u65B9\u5F0F\u9A8C\u8BC1\uFF1A\u6240\u62A5\u73B0\u8C61\u4E0D\u518D\u51FA\u73B0", "\u6539\u52A8\u6CA1\u6709\u660E\u663E\u6253\u574F\u76F4\u63A5\u76F8\u90BB\u7684\u8DEF\u5F84"],
234811
234982
  plan
@@ -234836,7 +235007,7 @@ function adoptExternalResolve(ctx) {
234836
235007
  artifactType: "research",
234837
235008
  title,
234838
235009
  brief,
234839
- goal: title,
235010
+ goal: workorderGoal(title, "\u56F4\u7ED5\u300C", "\u300D\u501F\u9274\u5916\u90E8\u65B9\u6848\uFF0C\u5B8C\u6210\u843D\u5730\u5E76\u9A8C\u8BC1\u6548\u679C\u3002"),
234840
235011
  ...manager ? { manager } : {},
234841
235012
  acceptanceCriteria: [
234842
235013
  "\u843D\u5730\u65B9\u6848\u9010\u6761\u8BF4\u660E\u4E86\u4E3A\u4EC0\u4E48\u4E0D\u76F4\u63A5\u91C7\u7528\u5DF2\u77E5\u89E3\u6CD5\uFF0C\u5E76\u5199\u660E\u6211\u4EEC\u65B9\u6848\u7684\u4EE3\u4EF7",
@@ -234867,7 +235038,7 @@ function simpleTaskResolve(ctx) {
234867
235038
  artifactType: "research",
234868
235039
  title,
234869
235040
  brief,
234870
- goal: title,
235041
+ goal: workorderGoal(title, "\u5B8C\u6210\u300C", "\u300D\uFF0C\u4EA4\u4ED8\u53D1\u8D77\u4EBA\u53EF\u76F4\u63A5\u4F7F\u7528\u7684\u7ED3\u679C\u3002"),
234871
235042
  ...manager ? { manager } : {},
234872
235043
  plan
234873
235044
  };
@@ -263976,7 +264147,8 @@ ${nodeFault}` : "");
263976
264147
  baseUrl: server.baseUrl,
263977
264148
  operatorToken,
263978
264149
  __coordinatorSpawnProbe: coordinatorSpawnProbe,
263979
- close: async () => {
264150
+ /** @param opts.drainMs 优雅停机排空预算,原样透传给 HTTP 层(见 server.ts 的 close)。缺省 = 老行为。 */
264151
+ close: async (opts2) => {
263980
264152
  for (const timer of dispatchTimers) clearInterval(timer);
263981
264153
  clearInterval(slaTimer);
263982
264154
  escalationDeliveryWorker.stop();
@@ -263995,7 +264167,7 @@ ${nodeFault}` : "");
263995
264167
  if (boardWatchTimer) clearInterval(boardWatchTimer);
263996
264168
  clearInterval(serverHeartbeatTimer);
263997
264169
  hub?.close();
263998
- await server.close();
264170
+ await server.close(opts2 ?? {});
263999
264171
  if (pgPool) await pgPool.end().catch(() => {
264000
264172
  });
264001
264173
  releaseServeLock(lock);
@@ -268214,9 +268386,25 @@ async function runCli(argv, println = console.log, progressln = console.error) {
268214
268386
  println(`oasis serve \u5C31\u7EEA\uFF1Ahttp://0.0.0.0:${port2}\uFF08\u672C\u673A\u8BBF\u95EE\u7528 http://127.0.0.1:${port2}\uFF09`);
268215
268387
  println(`operator token \u5DF2\u5199\u5165 ${path35.join(dir, "operator-token")}\uFF08\u672C\u673A CLI \u81EA\u52A8\u8BFB\u53D6\uFF09`);
268216
268388
  if (flags.get("dispatch") === "true") println(`\u8C03\u5EA6\u5FAA\u73AF\u5DF2\u542F\u52A8\uFF08claude-code adapter\uFF09`);
268217
- await new Promise(() => {
268389
+ const drainMs = Number(process.env["OASIS_SHUTDOWN_DRAIN_MS"] ?? "3000");
268390
+ const shutdownBudgetMs = drainMs + 5e3;
268391
+ await new Promise((resolve10) => {
268392
+ let stopping = false;
268393
+ const stop = (signal) => {
268394
+ if (stopping) return;
268395
+ stopping = true;
268396
+ println(`[serve] \u6536\u5230 ${signal}\uFF0C\u5F00\u59CB\u4F18\u96C5\u505C\u673A\uFF08\u6392\u7A7A\u9884\u7B97 ${drainMs}ms\uFF09\u2026`);
268397
+ const hardExit = setTimeout(() => {
268398
+ println(`[serve] \u505C\u673A\u603B\u9884\u7B97 ${shutdownBudgetMs}ms \u7528\u5C3D\uFF0C\u5F3A\u5236\u9000\u51FA`);
268399
+ process.exit(0);
268400
+ }, shutdownBudgetMs);
268401
+ hardExit.unref?.();
268402
+ void handle.close({ drainMs }).then(() => println("[serve] \u5DF2\u4F18\u96C5\u505C\u673A")).catch((err) => println(`[serve] \u505C\u673A\u8FC7\u7A0B\u51FA\u9519\uFF08\u7167\u5E38\u9000\u51FA\uFF09\uFF1A${String(err)}`)).finally(() => resolve10());
268403
+ };
268404
+ process.once("SIGTERM", () => stop("SIGTERM"));
268405
+ process.once("SIGINT", () => stop("SIGINT"));
268218
268406
  });
268219
- return;
268407
+ process.exit(0);
268220
268408
  }
268221
268409
  const base = flags.get("server") ?? process.env["OASIS_SERVER"] ?? "http://127.0.0.1:7320";
268222
268410
  const tokenFile = path35.join(dir, "operator-token");
@@ -271244,7 +271432,7 @@ function shimScript() {
271244
271432
  }
271245
271433
 
271246
271434
  // src/index.ts
271247
- var PKG_VERSION = true ? "2.2.12" : "dev";
271435
+ var PKG_VERSION = true ? "2.2.14" : "dev";
271248
271436
  var LOCAL_BIN = localBin();
271249
271437
  var NPM_PREFIX = npmPrefix();
271250
271438
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "ae18ecf5995f37b7a3f14b601e3a87c21d91ab03"
29
+ "sourceHead": "c6b9e871786ca57b2d1b01293c368ed043db700f"
30
30
  }
31
31
  }