oasis_test 0.1.117 → 0.1.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1086 -195
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2866,9 +2866,11 @@ var init_execution_continuity = __esm({
|
|
|
2866
2866
|
});
|
|
2867
2867
|
|
|
2868
2868
|
// ../contract/src/automation.ts
|
|
2869
|
+
var AUTOMATION_EVENT_NAMES;
|
|
2869
2870
|
var init_automation = __esm({
|
|
2870
2871
|
"../contract/src/automation.ts"() {
|
|
2871
2872
|
"use strict";
|
|
2873
|
+
AUTOMATION_EVENT_NAMES = ["workorder.completed"];
|
|
2872
2874
|
}
|
|
2873
2875
|
});
|
|
2874
2876
|
|
|
@@ -4799,6 +4801,11 @@ function createPolicy(cfg) {
|
|
|
4799
4801
|
}
|
|
4800
4802
|
};
|
|
4801
4803
|
}
|
|
4804
|
+
function policyGetter(source, fallback = CODE_DEFAULT_POLICY) {
|
|
4805
|
+
if (typeof source === "function") return source;
|
|
4806
|
+
if (source) return () => source;
|
|
4807
|
+
return () => fallback;
|
|
4808
|
+
}
|
|
4802
4809
|
var MINUTE, HOUR, DAY, AGENT_WORK_DEADLINE_MS, AGENT_REVIEW_DEADLINE_MS, AGENT_REVIEW_MAX_RETRIES, AGENT_WORK_MAX_RETRIES, RETRY_BASE_MS, RUNTIME_MAX_RETRIES, DEFAULT_IDLE_BY_RUNTIME, DEFAULT_IDLE_MS, DEFAULT_WALL_CLOCK_BY_RUNTIME, DEFAULT_WALL_CLOCK_MS, DEFAULT_HUMAN_CADENCE, CODE_DEFAULT_POLICY;
|
|
4803
4810
|
var init_policy = __esm({
|
|
4804
4811
|
"../timeout-policy/src/policy.ts"() {
|
|
@@ -7526,7 +7533,11 @@ function tryToRun(state, nodeId, ctx, events) {
|
|
|
7526
7533
|
nodeId,
|
|
7527
7534
|
workId,
|
|
7528
7535
|
reasons,
|
|
7529
|
-
|
|
7536
|
+
// ★ 基线(配套一):base=上一已验收版(latestAcceptId),parents[0]=最近一个有产出版
|
|
7537
|
+
//(latestProposedWorkId)。两者解耦:中间夹超时/零产出空壳时 base 仍在上一已验收版、parents 跳过空壳。
|
|
7538
|
+
// 不用 latestWorkId——它含空壳,会把基线钉到未验收/无产出的 work 上(isStale 比已验收 head,错)。
|
|
7539
|
+
continuesWorkId: node.latestAcceptId,
|
|
7540
|
+
parentWorkId: node.latestProposedWorkId,
|
|
7530
7541
|
// 工单 e4a43a42(接续只对短时重试成立):**已收口过的节点被重新激活 = 返工**(变更请求/驳回/上游
|
|
7531
7542
|
// 变更把已 accept 的节点重开)→ 起全新会话,别接续那场过时的旧对话;**从未收口的节点** = 首跑或
|
|
7532
7543
|
// 失败后重试,接续 WIP 是对的。判据 = 有没有 latest_accept。派发层据此决定 resumeSession。
|
|
@@ -7565,6 +7576,10 @@ function inputsMatch(state, node, latestWork) {
|
|
|
7565
7576
|
const latestInputs = new Set(state.workInputsOf(latestWork.id).map((i) => i.upstreamWorkId));
|
|
7566
7577
|
return currInputs.size === latestInputs.size && [...currInputs].every((x2) => latestInputs.has(x2));
|
|
7567
7578
|
}
|
|
7579
|
+
function reviewMaxRetriesOf(policy) {
|
|
7580
|
+
const act = policy.resolve({ executor: "agent", scenario: "review" }).timeoutAction;
|
|
7581
|
+
return act.kind === "kill-rebuild" ? act.maxRetries : AGENT_REVIEW_MAX_RETRIES;
|
|
7582
|
+
}
|
|
7568
7583
|
function processReviews(state, nodeId, workId, ctx, events) {
|
|
7569
7584
|
const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
|
|
7570
7585
|
const reqs = state.requirementsOf(nodeId);
|
|
@@ -7614,7 +7629,7 @@ function processReviews(state, nodeId, workId, ctx, events) {
|
|
|
7614
7629
|
}
|
|
7615
7630
|
} else if (info.status === "failed") {
|
|
7616
7631
|
const req = reqs.find((r) => r.reviewGroup === g2);
|
|
7617
|
-
const overLimit = countFailedForReviewGroup(g2, reviews) >
|
|
7632
|
+
const overLimit = countFailedForReviewGroup(g2, reviews) > reviewMaxRetriesOf(policy);
|
|
7618
7633
|
if (overLimit && classify(req.reviewerActorId) === "agent") {
|
|
7619
7634
|
const lrId = req.latestReviewId;
|
|
7620
7635
|
if (lrId) events.push({
|
|
@@ -7901,6 +7916,9 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
|
|
|
7901
7916
|
outcome_detail jsonb,
|
|
7902
7917
|
session_ref text,
|
|
7903
7918
|
continues_work_id text,
|
|
7919
|
+
-- \u2605 \u8C31\u7CFB\u4EB2\u672C\uFF08parents[0]\uFF09\uFF1Awork \u5EFA\u65F6\u7684\u300C\u6700\u8FD1\u4E00\u4E2A\u6709\u4EA7\u51FA\u7248\u300D\uFF08node.latest_proposed_work_id\uFF09\u3002
|
|
7920
|
+
-- \u4E0E continues_work_id\uFF08base=\u5DF2\u9A8C\u6536\u7248\uFF09\u89E3\u8026\u2014\u2014\u4E24\u8005\u8BED\u4E49\u4E0D\u540C\u3001\u53D6\u503C\u5E38\u5F02\uFF08\u4E2D\u95F4\u5939\u7A7A\u58F3\u65F6\u5C24\u5176\u5982\u6B64\uFF09\u3002
|
|
7921
|
+
parent_work_id text,
|
|
7904
7922
|
-- \u2605 work \u5C5E\u4E8E\u54EA\u6761\u9053\uFF08\u6B63\u5411\u3001\u663E\u5F0F\u4E8B\u5B9E\uFF09\uFF1A'main'=\u8282\u70B9\u4E3B\u94FE\u4EA7\u51FA / 'reply'=\u56DE\u4FE1\u8F6E\u3002\u53D6\u4EE3\u65E7\u7684\u5426\u5B9A\u5F0F\u5224\u636E
|
|
7905
7923
|
-- "reply_to_issue_id IS NULL"\u3002\u5B58\u91CF\u884C\u6309 reply_to_issue_id \u56DE\u586B\uFF08\u89C1\u4E0B\u65B9\u8FC1\u79FB\u6BB5\uFF09\u3002
|
|
7906
7924
|
lane text CHECK (lane IN ('main','reply')),
|
|
@@ -8078,6 +8096,10 @@ CREATE TABLE IF NOT EXISTS ${schema}.event_handler_log (
|
|
|
8078
8096
|
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
|
|
8079
8097
|
ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
|
|
8080
8098
|
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS status text;
|
|
8099
|
+
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS parent_work_id text;
|
|
8100
|
+
-- \u5B58\u91CF\u56DE\u586B\uFF1Aparent_work_id \u7F3A\u7701\u6CBF\u7528 continues_work_id\uFF08\u65E7\u6865 parents=[continues_work_id] \u7684\u65E2\u6709\u8C31\u7CFB\uFF09\uFF0C
|
|
8101
|
+
-- \u65B0 work \u624D\u5199\u300C\u6700\u8FD1\u6709\u4EA7\u51FA\u7248\u300D\uFF08latest_proposed_work_id\uFF09\u5B9E\u73B0 base/parents \u89E3\u8026\u3002
|
|
8102
|
+
UPDATE ${schema}.works SET parent_work_id = continues_work_id WHERE parent_work_id IS NULL;
|
|
8081
8103
|
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS agent_handoff_attempt_id text;
|
|
8082
8104
|
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_status text;
|
|
8083
8105
|
ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_recorded_at timestamptz;
|
|
@@ -9427,7 +9449,7 @@ var require_utils2 = __commonJS({
|
|
|
9427
9449
|
var nodeCrypto = require("crypto");
|
|
9428
9450
|
module2.exports = {
|
|
9429
9451
|
postgresMd5PasswordHash,
|
|
9430
|
-
randomBytes:
|
|
9452
|
+
randomBytes: randomBytes8,
|
|
9431
9453
|
deriveKey,
|
|
9432
9454
|
sha256,
|
|
9433
9455
|
hashByName,
|
|
@@ -9437,7 +9459,7 @@ var require_utils2 = __commonJS({
|
|
|
9437
9459
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
9438
9460
|
var subtleCrypto = webCrypto.subtle;
|
|
9439
9461
|
var textEncoder = new TextEncoder();
|
|
9440
|
-
function
|
|
9462
|
+
function randomBytes8(length) {
|
|
9441
9463
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
9442
9464
|
}
|
|
9443
9465
|
async function md5(string5) {
|
|
@@ -13569,6 +13591,7 @@ var init_store_postgres = __esm({
|
|
|
13569
13591
|
outcomeDetail: w2.outcome_detail,
|
|
13570
13592
|
sessionRef: w2.session_ref,
|
|
13571
13593
|
continuesWorkId: w2.continues_work_id,
|
|
13594
|
+
parentWorkId: w2.parent_work_id,
|
|
13572
13595
|
// ★ lane 兜底(存量行迁移前 / 迁移与回填之间的竞态窗):无值时按 reply_to_issue_id 派生,
|
|
13573
13596
|
// 保证 TS 侧 `Work.lane` 恒非空。reply_to_issue_id 在读写映射层可直接读(护栏白名单)。
|
|
13574
13597
|
lane: w2.lane ?? (w2.reply_to_issue_id == null ? "main" : "reply"),
|
|
@@ -13784,10 +13807,10 @@ var init_store_postgres = __esm({
|
|
|
13784
13807
|
await this.c.query(
|
|
13785
13808
|
`INSERT INTO ${this.s}.works
|
|
13786
13809
|
(id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
|
|
13787
|
-
outcome_detail,session_ref,continues_work_id,reply_to_issue_id,output_version_no,
|
|
13810
|
+
outcome_detail,session_ref,continues_work_id,parent_work_id,reply_to_issue_id,output_version_no,
|
|
13788
13811
|
node_version,conclusion,proposal_reason,last_activity_at,frozen_issue_ids,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
|
|
13789
13812
|
acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane)
|
|
13790
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32)
|
|
13813
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
|
|
13791
13814
|
ON CONFLICT (id) DO NOTHING`,
|
|
13792
13815
|
[
|
|
13793
13816
|
w2.id,
|
|
@@ -13805,6 +13828,7 @@ var init_store_postgres = __esm({
|
|
|
13805
13828
|
w2.outcomeDetail ? JSON.stringify(w2.outcomeDetail) : null,
|
|
13806
13829
|
w2.sessionRef,
|
|
13807
13830
|
w2.continuesWorkId,
|
|
13831
|
+
w2.parentWorkId,
|
|
13808
13832
|
w2.replyToIssueId ?? null,
|
|
13809
13833
|
w2.outputVersionNo,
|
|
13810
13834
|
w2.nodeVersion,
|
|
@@ -14596,6 +14620,7 @@ var init_work = __esm({
|
|
|
14596
14620
|
outcomeDetail: null,
|
|
14597
14621
|
sessionRef: null,
|
|
14598
14622
|
continuesWorkId: e.continuesWorkId,
|
|
14623
|
+
parentWorkId: e.parentWorkId ?? null,
|
|
14599
14624
|
replyToIssueId: e.replyToIssueId,
|
|
14600
14625
|
outputVersionNo: null,
|
|
14601
14626
|
conclusion: null,
|
|
@@ -14634,6 +14659,7 @@ var init_work = __esm({
|
|
|
14634
14659
|
outcomeDetail: null,
|
|
14635
14660
|
sessionRef: null,
|
|
14636
14661
|
continuesWorkId: e.continuesWorkId,
|
|
14662
|
+
parentWorkId: e.parentWorkId ?? null,
|
|
14637
14663
|
outputVersionNo: null,
|
|
14638
14664
|
conclusion: null,
|
|
14639
14665
|
frozenIssueIds: frozen.length > 0 ? frozen : null,
|
|
@@ -15410,7 +15436,8 @@ var init_bus = __esm({
|
|
|
15410
15436
|
io;
|
|
15411
15437
|
clock;
|
|
15412
15438
|
config;
|
|
15413
|
-
policy
|
|
15439
|
+
/** 取当前策略(不是策略本身)——见 {@link EngineBusOptions.policy} 的热载不变量。 */
|
|
15440
|
+
policyOf;
|
|
15414
15441
|
registry;
|
|
15415
15442
|
maxEffectAttempts;
|
|
15416
15443
|
stuckAppliedRecoveryMs;
|
|
@@ -15422,7 +15449,7 @@ var init_bus = __esm({
|
|
|
15422
15449
|
this.io = opts.io ?? NOOP_IO;
|
|
15423
15450
|
this.clock = opts.clock ?? SYSTEM_CLOCK;
|
|
15424
15451
|
this.config = opts.config ?? DEFAULT_CONFIG;
|
|
15425
|
-
this.
|
|
15452
|
+
this.policyOf = policyGetter(opts.policy);
|
|
15426
15453
|
this.registry = new HandlerRegistry(opts.handlers);
|
|
15427
15454
|
this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
|
|
15428
15455
|
this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
|
|
@@ -15811,7 +15838,7 @@ var init_bus = __esm({
|
|
|
15811
15838
|
if (!snapshot) return { emitted: 0, terminated: false };
|
|
15812
15839
|
if (snapshot.workorder.lifecycle !== "active") return { emitted: 0, terminated: false };
|
|
15813
15840
|
const state = new WorkorderState(snapshot);
|
|
15814
|
-
const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.
|
|
15841
|
+
const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.policyOf() };
|
|
15815
15842
|
const { events, terminated } = scan(state, scanCtx);
|
|
15816
15843
|
await tx.applyMutations(workorderId, state.mutations);
|
|
15817
15844
|
const allEvents = [...state.emitted, ...events];
|
|
@@ -15958,8 +15985,9 @@ function mapWorkStateToRevisionState(w2, artifactCount) {
|
|
|
15958
15985
|
case "success":
|
|
15959
15986
|
if (w2.acceptanceState === "accepted") return "merged";
|
|
15960
15987
|
if (w2.acceptanceState === "rejected") return "rejected";
|
|
15988
|
+
if (w2.conclusion == null) return "working";
|
|
15961
15989
|
return "proposed";
|
|
15962
|
-
//
|
|
15990
|
+
// 已 conclude、未裁决
|
|
15963
15991
|
// ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
|
|
15964
15992
|
// superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
|
|
15965
15993
|
// 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
|
|
@@ -16037,7 +16065,9 @@ function workToRevision(w2, workInputs, artifactFiles) {
|
|
|
16037
16065
|
id: w2.id,
|
|
16038
16066
|
artifactId: w2.nodeId,
|
|
16039
16067
|
base: w2.continuesWorkId,
|
|
16040
|
-
|
|
16068
|
+
// ★ 配套一:base 与 parents 解耦——base=上一已验收版(continuesWorkId),parents[0]=最近一个有产出版
|
|
16069
|
+
//(parentWorkId)。两者可不同:中间夹超时/零产出空壳时 base 不动、parents 跳过空壳指向上一个有产出。
|
|
16070
|
+
parents: w2.parentWorkId ? [w2.parentWorkId] : [],
|
|
16041
16071
|
contentRef,
|
|
16042
16072
|
contentKind,
|
|
16043
16073
|
basedOn: Object.keys(basedOn).length > 0 ? basedOn : void 0,
|
|
@@ -16256,6 +16286,16 @@ function bridgeLoadReadModel(input) {
|
|
|
16256
16286
|
for (const w2 of allWorks) {
|
|
16257
16287
|
revisions.set(w2.id, workToRevision(w2, allWorkInputs, artifactFiles));
|
|
16258
16288
|
}
|
|
16289
|
+
for (const r of revisions.values()) {
|
|
16290
|
+
if (r.state !== "proposed") continue;
|
|
16291
|
+
for (const parentId of r.parents) {
|
|
16292
|
+
const parent = revisions.get(parentId);
|
|
16293
|
+
if (parent && parent.state === "proposed" && (parent.part ?? void 0) === (r.part ?? void 0)) {
|
|
16294
|
+
parent.state = "superseded";
|
|
16295
|
+
parent.supersededBy = r.id;
|
|
16296
|
+
}
|
|
16297
|
+
}
|
|
16298
|
+
}
|
|
16259
16299
|
const recipientsByIssue = /* @__PURE__ */ new Map();
|
|
16260
16300
|
for (const r of allIssueRecipients) {
|
|
16261
16301
|
const list = recipientsByIssue.get(r.issueId) ?? [];
|
|
@@ -17304,10 +17344,12 @@ var init_kernel_bridge = __esm({
|
|
|
17304
17344
|
nodeId: args.artifactId,
|
|
17305
17345
|
workId: revisionId,
|
|
17306
17346
|
reasons: [{ kind: "initial" }],
|
|
17307
|
-
// ★ base 三态:undefined
|
|
17308
|
-
//
|
|
17347
|
+
// ★ base 三态:undefined=缺省续写(基「上一已验收版」node.latestAcceptId——isStale 比 concludedHead,
|
|
17348
|
+
// 只有 latestAcceptId 对;旧口径 artifact.currentRev=latestWorkId 可能是一条未验收/空壳 work);null=并发
|
|
17309
17349
|
// 作者显式声明 base=∅(autoland stale 兜底:不落、留队);具体 revisionId=钉旧版。
|
|
17310
|
-
|
|
17350
|
+
// ★ parents[0]=「最近一个有产出版」node.latestProposedWorkId(跳过超时/零产出空壳),与 base 解耦。
|
|
17351
|
+
continuesWorkId: args.base !== void 0 ? args.base : nodeRow?.latestAcceptId ?? null,
|
|
17352
|
+
parentWorkId: nodeRow?.latestProposedWorkId ?? null
|
|
17311
17353
|
}
|
|
17312
17354
|
});
|
|
17313
17355
|
workId = revisionId;
|
|
@@ -40107,7 +40149,7 @@ var require_websocket = __commonJS({
|
|
|
40107
40149
|
var http2 = require("http");
|
|
40108
40150
|
var net = require("net");
|
|
40109
40151
|
var tls = require("tls");
|
|
40110
|
-
var { randomBytes:
|
|
40152
|
+
var { randomBytes: randomBytes8, createHash: createHash21 } = require("crypto");
|
|
40111
40153
|
var { Duplex, Readable } = require("stream");
|
|
40112
40154
|
var { URL: URL2 } = require("url");
|
|
40113
40155
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -40645,7 +40687,7 @@ var require_websocket = __commonJS({
|
|
|
40645
40687
|
}
|
|
40646
40688
|
}
|
|
40647
40689
|
const defaultPort = isSecure ? 443 : 80;
|
|
40648
|
-
const key =
|
|
40690
|
+
const key = randomBytes8(16).toString("base64");
|
|
40649
40691
|
const request2 = isSecure ? https.request : http2.request;
|
|
40650
40692
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
40651
40693
|
let perMessageDeflate;
|
|
@@ -40775,7 +40817,7 @@ var require_websocket = __commonJS({
|
|
|
40775
40817
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
40776
40818
|
return;
|
|
40777
40819
|
}
|
|
40778
|
-
const digest =
|
|
40820
|
+
const digest = createHash21("sha1").update(key + GUID).digest("base64");
|
|
40779
40821
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
40780
40822
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
40781
40823
|
return;
|
|
@@ -41144,7 +41186,7 @@ var require_websocket_server = __commonJS({
|
|
|
41144
41186
|
var EventEmitter = require("events");
|
|
41145
41187
|
var http2 = require("http");
|
|
41146
41188
|
var { Duplex } = require("stream");
|
|
41147
|
-
var { createHash:
|
|
41189
|
+
var { createHash: createHash21 } = require("crypto");
|
|
41148
41190
|
var extension2 = require_extension();
|
|
41149
41191
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
41150
41192
|
var subprotocol2 = require_subprotocol();
|
|
@@ -41451,7 +41493,7 @@ var require_websocket_server = __commonJS({
|
|
|
41451
41493
|
);
|
|
41452
41494
|
}
|
|
41453
41495
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
41454
|
-
const digest =
|
|
41496
|
+
const digest = createHash21("sha1").update(key + GUID).digest("base64");
|
|
41455
41497
|
const headers = [
|
|
41456
41498
|
"HTTP/1.1 101 Switching Protocols",
|
|
41457
41499
|
"Upgrade: websocket",
|
|
@@ -128706,13 +128748,13 @@ ${block}
|
|
|
128706
128748
|
throw err;
|
|
128707
128749
|
}
|
|
128708
128750
|
const delay = base * Math.pow(3, attempt - 1);
|
|
128709
|
-
yield
|
|
128751
|
+
yield sleep5(delay);
|
|
128710
128752
|
}
|
|
128711
128753
|
}
|
|
128712
128754
|
throw lastErr;
|
|
128713
128755
|
});
|
|
128714
128756
|
}
|
|
128715
|
-
function
|
|
128757
|
+
function sleep5(ms) {
|
|
128716
128758
|
return new Promise((r) => setTimeout(r, ms));
|
|
128717
128759
|
}
|
|
128718
128760
|
var DEFAULT_CHUNK_LIMIT = 3500;
|
|
@@ -158018,6 +158060,40 @@ var init_resume_classify = __esm({
|
|
|
158018
158060
|
}
|
|
158019
158061
|
});
|
|
158020
158062
|
|
|
158063
|
+
// ../adapters/src/_core/root-bypass.ts
|
|
158064
|
+
function decideRootBypass(sources) {
|
|
158065
|
+
if (sources.uid !== 0) return { inject: false, reason: "not-root" };
|
|
158066
|
+
if (sources.env["IS_SANDBOX"] === "1") return { inject: false, reason: "already-sandboxed" };
|
|
158067
|
+
if (sources.env["CLAUDE_CODE_BUBBLEWRAP"]) return { inject: false, reason: "bubblewrap" };
|
|
158068
|
+
const sw = (sources.env["OASIS_ALLOW_ROOT_BYPASS"] ?? "").trim().toLowerCase();
|
|
158069
|
+
if (OFF_VALUES.has(sw)) return { inject: false, reason: "disabled" };
|
|
158070
|
+
return { inject: true, reason: "root-needs-sandbox-flag" };
|
|
158071
|
+
}
|
|
158072
|
+
function rootBypassEnv(runtime, sources = { uid: process.getuid?.(), env: process.env }) {
|
|
158073
|
+
const d = decideRootBypass(sources);
|
|
158074
|
+
if (d.inject || d.reason === "disabled") {
|
|
158075
|
+
const key = `${runtime}:${d.reason}`;
|
|
158076
|
+
if (!loggedOnce.has(key)) {
|
|
158077
|
+
loggedOnce.add(key);
|
|
158078
|
+
console.warn(`[adapter:${runtime}] ` + JSON.stringify({
|
|
158079
|
+
event: "root-bypass",
|
|
158080
|
+
injected: d.inject,
|
|
158081
|
+
reason: d.reason,
|
|
158082
|
+
note: d.inject ? "\u4EE5 root \u8FD0\u884C\uFF1A\u6CE8\u5165 IS_SANDBOX=1 \u653E\u884C bypassPermissions\uFF08\u5173\u95ED\uFF1AOASIS_ALLOW_ROOT_BYPASS=off\uFF09" : "\u4EE5 root \u8FD0\u884C\u4E14 OASIS_ALLOW_ROOT_BYPASS=off\uFF1ACLI \u5C06\u62D2\u7EDD bypassPermissions\uFF0C\u4F1A\u8BDD\u4F1A\u7ACB\u5373\u9000\u51FA"
|
|
158083
|
+
}));
|
|
158084
|
+
}
|
|
158085
|
+
}
|
|
158086
|
+
return d.inject ? { IS_SANDBOX: "1" } : {};
|
|
158087
|
+
}
|
|
158088
|
+
var OFF_VALUES, loggedOnce;
|
|
158089
|
+
var init_root_bypass = __esm({
|
|
158090
|
+
"../adapters/src/_core/root-bypass.ts"() {
|
|
158091
|
+
"use strict";
|
|
158092
|
+
OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false", "no"]);
|
|
158093
|
+
loggedOnce = /* @__PURE__ */ new Set();
|
|
158094
|
+
}
|
|
158095
|
+
});
|
|
158096
|
+
|
|
158021
158097
|
// ../adapters/src/_core/transcript.ts
|
|
158022
158098
|
function openTranscript(file) {
|
|
158023
158099
|
const out = fs8.createWriteStream(file);
|
|
@@ -158268,6 +158344,7 @@ var init_claude_code = __esm({
|
|
|
158268
158344
|
init_materialize_bundle();
|
|
158269
158345
|
init_container_runtime();
|
|
158270
158346
|
init_resume_classify();
|
|
158347
|
+
init_root_bypass();
|
|
158271
158348
|
init_session_paths();
|
|
158272
158349
|
init_transcript();
|
|
158273
158350
|
init_filter_extra_args();
|
|
@@ -158427,7 +158504,11 @@ var init_claude_code = __esm({
|
|
|
158427
158504
|
OASIS_SERVER: job.server.url,
|
|
158428
158505
|
OASIS_TOKEN: job.actorToken,
|
|
158429
158506
|
// OTel 遥测(默认关;resolveOtelEnv 决定开不开,runId 进 resource 属性自然归位到本次 run)。
|
|
158430
|
-
...otelEnv
|
|
158507
|
+
...otelEnv,
|
|
158508
|
+
// root 下放行 bypassPermissions(见 _core/root-bypass.ts:claude 的闸认 IS_SANDBOX=1)。
|
|
158509
|
+
// 铺在最后=系统强制层(同 OASIS_SERVER/TOKEN):员工 env 或工单 executionEnv 里的陈旧值
|
|
158510
|
+
// 不得把它顶掉。非 root / 宿主已给 / 显式关闭时是空对象,行为逐字节不变。
|
|
158511
|
+
...rootBypassEnv("claude-code")
|
|
158431
158512
|
},
|
|
158432
158513
|
stdio: ["pipe", "pipe", "pipe"]
|
|
158433
158514
|
});
|
|
@@ -161493,14 +161574,17 @@ async function materialize(job, cfg) {
|
|
|
161493
161574
|
const task = materializeBundle(dir, job.bundle);
|
|
161494
161575
|
return { id, dir, task };
|
|
161495
161576
|
}
|
|
161496
|
-
function buildEnv(job,
|
|
161577
|
+
function buildEnv(job, cfg) {
|
|
161497
161578
|
return {
|
|
161498
161579
|
...scrubLeakyEnv(process.env),
|
|
161499
|
-
...
|
|
161580
|
+
...cfg.env,
|
|
161500
161581
|
...job.env,
|
|
161501
161582
|
...job.wrapperPaths?.length ? { PATH: `${job.wrapperPaths.map((p2) => path12.dirname(p2)).join(path12.delimiter)}${path12.delimiter}${job.env?.["PATH"] ?? process.env["PATH"] ?? ""}` } : {},
|
|
161502
161583
|
OASIS_SERVER: job.server.url,
|
|
161503
|
-
OASIS_TOKEN: job.actorToken
|
|
161584
|
+
OASIS_TOKEN: job.actorToken,
|
|
161585
|
+
// root 下放行 bypassPermissions(仅 claude 家族 runtime 声明;见 _core/root-bypass.ts)。
|
|
161586
|
+
// 与 OASIS_SERVER/TOKEN 同属系统强制层:员工 env 顶不掉。
|
|
161587
|
+
...cfg.rootBypassRuntime ? rootBypassEnv(cfg.rootBypassRuntime) : {}
|
|
161504
161588
|
};
|
|
161505
161589
|
}
|
|
161506
161590
|
function makeFinish(id, cfg, transcriptRefPtr, transcriptOut, dir, workdirKey, runtimeSessionId, exitCbs) {
|
|
@@ -161559,7 +161643,7 @@ async function runStreamJson(job, cfg) {
|
|
|
161559
161643
|
stdio: ["ignore", "pipe", "pipe"]
|
|
161560
161644
|
}) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
|
|
161561
161645
|
cwd: dir,
|
|
161562
|
-
env: buildEnv(job, cfg
|
|
161646
|
+
env: buildEnv(job, cfg),
|
|
161563
161647
|
stdio: ["ignore", "pipe", "pipe"]
|
|
161564
161648
|
});
|
|
161565
161649
|
if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
|
|
@@ -161659,7 +161743,7 @@ async function runOneShotText(job, cfg) {
|
|
|
161659
161743
|
stdio: ["ignore", "pipe", "pipe"]
|
|
161660
161744
|
}) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
|
|
161661
161745
|
cwd: dir,
|
|
161662
|
-
env: buildEnv(job, cfg
|
|
161746
|
+
env: buildEnv(job, cfg),
|
|
161663
161747
|
stdio: ["ignore", "pipe", "pipe"]
|
|
161664
161748
|
});
|
|
161665
161749
|
if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
|
|
@@ -161743,6 +161827,7 @@ var init_subprocess = __esm({
|
|
|
161743
161827
|
init_session_paths();
|
|
161744
161828
|
init_transcript();
|
|
161745
161829
|
init_filter_extra_args();
|
|
161830
|
+
init_root_bypass();
|
|
161746
161831
|
}
|
|
161747
161832
|
});
|
|
161748
161833
|
|
|
@@ -161859,7 +161944,10 @@ var init_codebuddy = __esm({
|
|
|
161859
161944
|
workRoot: opts.workRoot,
|
|
161860
161945
|
cleanupWorkdir: opts.cleanupWorkdir,
|
|
161861
161946
|
env: opts.env,
|
|
161862
|
-
extraArgs: opts.extraArgs
|
|
161947
|
+
extraArgs: opts.extraArgs,
|
|
161948
|
+
// CodeBuddy 是 Claude Code 分叉,走同一个 bypassPermissions flag,root 下大概率撞同一条闸
|
|
161949
|
+
// (claude 侧已实测)。声明后由 _core/root-bypass.ts 在 root 下注入 IS_SANDBOX=1。
|
|
161950
|
+
rootBypassRuntime: "codebuddy"
|
|
161863
161951
|
});
|
|
161864
161952
|
}
|
|
161865
161953
|
};
|
|
@@ -168058,6 +168146,10 @@ var init_memory_automation_store = __esm({
|
|
|
168058
168146
|
hasSigningSecret: false
|
|
168059
168147
|
};
|
|
168060
168148
|
}
|
|
168149
|
+
if (input.kind === "event" && input.event) {
|
|
168150
|
+
t.enabled = false;
|
|
168151
|
+
t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
|
|
168152
|
+
}
|
|
168061
168153
|
return t;
|
|
168062
168154
|
}
|
|
168063
168155
|
async replaceTriggers(automationId, triggers) {
|
|
@@ -168085,6 +168177,7 @@ var init_memory_automation_store = __esm({
|
|
|
168085
168177
|
if (t.webhook && patch.webhook && "eventFilters" in patch.webhook) {
|
|
168086
168178
|
t.webhook.eventFilters = patch.webhook.eventFilters ?? null;
|
|
168087
168179
|
}
|
|
168180
|
+
if (t.event && patch.event?.events) t.event.events = [...patch.event.events];
|
|
168088
168181
|
return structuredClone(t);
|
|
168089
168182
|
}
|
|
168090
168183
|
async deleteTrigger(triggerId) {
|
|
@@ -168113,6 +168206,33 @@ var init_memory_automation_store = __esm({
|
|
|
168113
168206
|
if (t) t.publishedBy = publishedBy;
|
|
168114
168207
|
}
|
|
168115
168208
|
}
|
|
168209
|
+
/* ---------- event 触发器(ADR 0163) ---------- */
|
|
168210
|
+
async listEventTriggers() {
|
|
168211
|
+
const out = [];
|
|
168212
|
+
for (const t of this.triggers.values()) {
|
|
168213
|
+
if (t.kind !== "event") continue;
|
|
168214
|
+
const a = this.automations.get(t.automationId);
|
|
168215
|
+
if (!a || !a.enabled || a.archivedAt) continue;
|
|
168216
|
+
out.push(structuredClone(t));
|
|
168217
|
+
}
|
|
168218
|
+
return out.sort((x2, y) => x2.id < y.id ? -1 : x2.id > y.id ? 1 : 0);
|
|
168219
|
+
}
|
|
168220
|
+
async completeEventBaseline(triggerId, _at) {
|
|
168221
|
+
const t = this.triggers.get(triggerId);
|
|
168222
|
+
if (!t?.event) return;
|
|
168223
|
+
t.event.baselineState = "ready";
|
|
168224
|
+
t.enabled = true;
|
|
168225
|
+
}
|
|
168226
|
+
async resetEventBaseline(triggerId) {
|
|
168227
|
+
const t = this.triggers.get(triggerId);
|
|
168228
|
+
if (!t?.event) return;
|
|
168229
|
+
t.event.baselineState = "aligning";
|
|
168230
|
+
t.enabled = false;
|
|
168231
|
+
}
|
|
168232
|
+
async findRunBySpawnedWorkspace(workspace) {
|
|
168233
|
+
const hits = [...this.runs.values()].filter((r) => r.spawnedWorkspaceId === workspace).sort((a, b2) => a.triggeredAt < b2.triggeredAt ? -1 : a.triggeredAt > b2.triggeredAt ? 1 : a.id < b2.id ? -1 : 1);
|
|
168234
|
+
return hits[0] ? structuredClone(hits[0]) : null;
|
|
168235
|
+
}
|
|
168116
168236
|
/* ---------- runs ---------- */
|
|
168117
168237
|
async createRun(run) {
|
|
168118
168238
|
if (run.triggerId && run.plannedAt) {
|
|
@@ -173812,7 +173932,9 @@ async function openNewEngine(opts) {
|
|
|
173812
173932
|
const bus = new EngineBus({
|
|
173813
173933
|
store,
|
|
173814
173934
|
io: NOOP_IO,
|
|
173815
|
-
clock: SYSTEM_CLOCK
|
|
173935
|
+
clock: SYSTEM_CLOCK,
|
|
173936
|
+
// 原样交给 bus(bus 内部归一成取值函数)——这里绝不能 call 一次存下来,那就把热载掐死了。
|
|
173937
|
+
policy: opts.policy
|
|
173816
173938
|
});
|
|
173817
173939
|
const kernel = await EngineKernel.open(store, bus, {
|
|
173818
173940
|
schema: opts.schemaDefs ?? [],
|
|
@@ -183313,7 +183435,11 @@ var init_readonly_bundle = __esm({
|
|
|
183313
183435
|
function makeSeededWorkorderCreator(deps) {
|
|
183314
183436
|
const genWorkspace = deps.genWorkspace ?? (() => `ws:wo-${(0, import_node_crypto33.randomUUID)().slice(0, 8)}`);
|
|
183315
183437
|
return async (input) => {
|
|
183316
|
-
const workspace = genWorkspace();
|
|
183438
|
+
const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
|
|
183439
|
+
if (input.idempotencyKey) {
|
|
183440
|
+
const existing = existingWorkorder(deps.kernel, workspace);
|
|
183441
|
+
if (existing) return existing;
|
|
183442
|
+
}
|
|
183317
183443
|
if (deps.artifactState && (input.projectId || deps.createProject)) {
|
|
183318
183444
|
await ensureWorkorderProject({
|
|
183319
183445
|
workOrderId: workspace,
|
|
@@ -183392,6 +183518,20 @@ function makeSeededWorkorderCreator(deps) {
|
|
|
183392
183518
|
return { workspace, rootArtifactId: briefId, spawned: planned.spawned };
|
|
183393
183519
|
};
|
|
183394
183520
|
}
|
|
183521
|
+
function workspaceForKey(key) {
|
|
183522
|
+
return `ws:wo-${(0, import_node_crypto33.createHash)("sha256").update(key).digest("hex").slice(0, 8)}`;
|
|
183523
|
+
}
|
|
183524
|
+
function existingWorkorder(kernel, workspace) {
|
|
183525
|
+
const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
|
|
183526
|
+
if (arts.length === 0) return null;
|
|
183527
|
+
const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
|
|
183528
|
+
return {
|
|
183529
|
+
workspace,
|
|
183530
|
+
rootArtifactId: seed.id,
|
|
183531
|
+
// role 取自 planner 的规划结果、不是 artifact 上的字段——重试路径拿不到也无所谓(可省)。
|
|
183532
|
+
spawned: arts.map((a) => ({ id: a.id, type: a.type, owner: a.owner }))
|
|
183533
|
+
};
|
|
183534
|
+
}
|
|
183395
183535
|
var import_node_crypto33, enc2;
|
|
183396
183536
|
var init_create_seeded_workorder = __esm({
|
|
183397
183537
|
"../server/src/domains/collab/create-seeded-workorder.ts"() {
|
|
@@ -186076,6 +186216,7 @@ function automationSubstantiveChange(before, after) {
|
|
|
186076
186216
|
}
|
|
186077
186217
|
function triggerSubstantiveChange(before, after) {
|
|
186078
186218
|
if (before.enabled !== after.enabled) return true;
|
|
186219
|
+
if (!jsonEq(before.event?.events ?? null, after.event?.events ?? null)) return true;
|
|
186079
186220
|
if ((before.schedule?.cron ?? null) !== (after.schedule?.cron ?? null)) return true;
|
|
186080
186221
|
if ((before.schedule?.tz ?? null) !== (after.schedule?.tz ?? null)) return true;
|
|
186081
186222
|
const filtersOf = (t) => t.webhook?.eventFilters ?? null;
|
|
@@ -186927,6 +187068,17 @@ function renderDate(at, tz) {
|
|
|
186927
187068
|
return at.toISOString().slice(0, 10);
|
|
186928
187069
|
}
|
|
186929
187070
|
}
|
|
187071
|
+
function eventCommandKey(runId) {
|
|
187072
|
+
return `automation-event:${runId}`;
|
|
187073
|
+
}
|
|
187074
|
+
function eventDispatchState(run) {
|
|
187075
|
+
const r = run.result;
|
|
187076
|
+
if (r && typeof r === "object" && !Array.isArray(r)) {
|
|
187077
|
+
const d = r["eventDispatch"];
|
|
187078
|
+
if (d && typeof d === "object" && !Array.isArray(d)) return d;
|
|
187079
|
+
}
|
|
187080
|
+
return {};
|
|
187081
|
+
}
|
|
186930
187082
|
function clipReply(reply) {
|
|
186931
187083
|
const t = reply.trim();
|
|
186932
187084
|
return t.length > CHAT_REPLY_MAX ? `${t.slice(0, CHAT_REPLY_MAX)}
|
|
@@ -186958,7 +187110,7 @@ function appendSourceNote(brief, automation, source, atIso) {
|
|
|
186958
187110
|
---
|
|
186959
187111
|
\u672C\u5DE5\u5355\u7531\u81EA\u52A8\u5316\u300C${automation.name}\u300D\u4E8E ${atIso} \u89E6\u53D1\uFF08\u6765\u6E90\uFF1A${source}\uFF09\u3002\u5F00\u5DE5\u540E\u8BF7\u628A\u5DE5\u5355\u6807\u9898\u6539\u6210\u51C6\u786E\u53CD\u6620\u5B9E\u9645\u5DE5\u4F5C\u7684\u63CF\u8FF0\u3002`;
|
|
186960
187112
|
}
|
|
186961
|
-
var import_node_crypto36, DATE_TOKEN, AutomationsService, CHAT_REPLY_MAX;
|
|
187113
|
+
var import_node_crypto36, DATE_TOKEN, AutomationsService, EVENT_RETRY_BASE_MS, EVENT_RETRY_MAX_MS, EVENT_RETRY_MAX_ATTEMPTS, eventAttempts, eventLastAttemptAt, eventLastError, CHAT_REPLY_MAX;
|
|
186962
187114
|
var init_service8 = __esm({
|
|
186963
187115
|
"../server/src/domains/automations/service.ts"() {
|
|
186964
187116
|
"use strict";
|
|
@@ -187051,7 +187203,7 @@ var init_service8 = __esm({
|
|
|
187051
187203
|
await seedScheduleNextRuns(this.store, [await this.store.getTrigger(t.id) ?? t].filter(Boolean), this.now());
|
|
187052
187204
|
return await this.store.getTrigger(t.id);
|
|
187053
187205
|
}
|
|
187054
|
-
/** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters)只转移**该行**。 */
|
|
187206
|
+
/** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters/events)只转移**该行**。 */
|
|
187055
187207
|
async updateTrigger(triggerId, patch, editor) {
|
|
187056
187208
|
const before = await this.store.getTrigger(triggerId);
|
|
187057
187209
|
if (!before) throw new Error(`trigger not found: ${triggerId}`);
|
|
@@ -187062,6 +187214,9 @@ var init_service8 = __esm({
|
|
|
187062
187214
|
if (after.kind === "schedule") {
|
|
187063
187215
|
await seedScheduleNextRuns(this.store, [after], this.now());
|
|
187064
187216
|
}
|
|
187217
|
+
if (after.kind === "event" && patch.enabled === true && !before.enabled) {
|
|
187218
|
+
await this.store.resetEventBaseline(triggerId);
|
|
187219
|
+
}
|
|
187065
187220
|
return await this.store.getTrigger(triggerId);
|
|
187066
187221
|
}
|
|
187067
187222
|
async deleteTrigger(triggerId, editor) {
|
|
@@ -187141,7 +187296,17 @@ var init_service8 = __esm({
|
|
|
187141
187296
|
};
|
|
187142
187297
|
const { run, created } = await this.store.createRun(base);
|
|
187143
187298
|
if (!created) return run;
|
|
187144
|
-
|
|
187299
|
+
return this.dispatchRun(automation, trigger, run, source, opts);
|
|
187300
|
+
}
|
|
187301
|
+
/**
|
|
187302
|
+
* 对一条**已占位的 pending run** 做准入 + 派发。
|
|
187303
|
+
*
|
|
187304
|
+
* 与 `fire()` 拆开是因为 event 触发必须「先原子占位、再决定派不派」(ADR 0163 §4.2 第 4/5 步):
|
|
187305
|
+
* 占位赢家才做权限/自环准入,输的那个直接停手。reconciler 重试也复用这里——同一 runId、
|
|
187306
|
+
* 同一下游命令键,取回同一副作用。
|
|
187307
|
+
*/
|
|
187308
|
+
async dispatchRun(automation, trigger, run, source, opts = {}) {
|
|
187309
|
+
const skip = await this.admissionSkip(automation, source);
|
|
187145
187310
|
if (skip) return this.settleSkip(run, skip.reasonCode, skip.error);
|
|
187146
187311
|
if (automation.executionMode === "chat") {
|
|
187147
187312
|
return this.fireChat(automation, trigger, run, opts);
|
|
@@ -187161,6 +187326,14 @@ var init_service8 = __esm({
|
|
|
187161
187326
|
return settled ?? run;
|
|
187162
187327
|
} catch (e) {
|
|
187163
187328
|
const msg = e instanceof Error ? e.message : String(e);
|
|
187329
|
+
if (source === "event") {
|
|
187330
|
+
await this.store.settleRun(run.id, {
|
|
187331
|
+
status: "pending",
|
|
187332
|
+
result: { eventDispatch: { attempts: eventAttempts(run) + 1, lastAttemptAt: this.now().toISOString(), lastError: msg } },
|
|
187333
|
+
completedAt: null
|
|
187334
|
+
});
|
|
187335
|
+
return await this.store.getRun(run.id);
|
|
187336
|
+
}
|
|
187164
187337
|
await this.store.settleRun(run.id, {
|
|
187165
187338
|
status: "failed",
|
|
187166
187339
|
error: msg,
|
|
@@ -187194,12 +187367,93 @@ var init_service8 = __esm({
|
|
|
187194
187367
|
});
|
|
187195
187368
|
return run;
|
|
187196
187369
|
}
|
|
187197
|
-
|
|
187370
|
+
/* ---------- 订阅触发(event,ADR 0163) ---------- */
|
|
187371
|
+
/**
|
|
187372
|
+
* 能力位:这条自动化现在能不能用订阅触发。**唯一真值在这里**——详情响应把它原样带给前端,
|
|
187373
|
+
* 前端只消费不推断(否则前后端两套规则必漂移)。
|
|
187374
|
+
*/
|
|
187375
|
+
eventTriggerCapability(automation) {
|
|
187376
|
+
if (automation.executionMode === "chat" && !this.deps.chatSupportsIdempotency) {
|
|
187377
|
+
return { supported: false, reason: "chat_no_idempotency" };
|
|
187378
|
+
}
|
|
187379
|
+
return { supported: true };
|
|
187380
|
+
}
|
|
187381
|
+
/**
|
|
187382
|
+
* 事件消费键占位:用同一把键原子落一条 run。
|
|
187383
|
+
* 唯一约束 `(automation_id, idempotency_key)` 负责多实例竞争——`created=false` 的那个实例停手。
|
|
187384
|
+
*/
|
|
187385
|
+
claimEventRun(automation, trigger, key, envelope, status = "pending", reasonCode = null, error2 = null) {
|
|
187386
|
+
const nowIso = this.now().toISOString();
|
|
187387
|
+
return this.store.createRun({
|
|
187388
|
+
id: this.genId("arun"),
|
|
187389
|
+
automationId: automation.id,
|
|
187390
|
+
triggerId: trigger.id,
|
|
187391
|
+
source: "event",
|
|
187392
|
+
status,
|
|
187393
|
+
spawnedWorkspaceId: null,
|
|
187394
|
+
spawnedArtifactId: null,
|
|
187395
|
+
plannedAt: null,
|
|
187396
|
+
deliveryId: null,
|
|
187397
|
+
idempotencyKey: key,
|
|
187398
|
+
ruleVersionId: null,
|
|
187399
|
+
payload: envelope,
|
|
187400
|
+
result: null,
|
|
187401
|
+
error: error2,
|
|
187402
|
+
reasonCode,
|
|
187403
|
+
triggeredAt: nowIso,
|
|
187404
|
+
completedAt: status === "skipped" ? nowIso : null
|
|
187405
|
+
});
|
|
187406
|
+
}
|
|
187407
|
+
/** 把一条占位成功的 event run 派出去(准入 → 建单/开会话),命令键固定为 automation-event:<runId>。 */
|
|
187408
|
+
dispatchEventRun(automation, trigger, run) {
|
|
187409
|
+
return this.dispatchRun(automation, trigger, run, "event", {
|
|
187410
|
+
...run.payload != null ? { envelope: run.payload } : {},
|
|
187411
|
+
commandKey: eventCommandKey(run.id)
|
|
187412
|
+
});
|
|
187413
|
+
}
|
|
187414
|
+
/** 已占位但被准入拒绝:落 skipped + reasonCode(**留痕**,不是静默丢弃)。 */
|
|
187415
|
+
skipEventRun(run, reasonCode, error2) {
|
|
187416
|
+
return this.settleSkip(run, reasonCode, error2);
|
|
187417
|
+
}
|
|
187418
|
+
/**
|
|
187419
|
+
* event pending 的可恢复派发(ADR 0163 §4.4)。
|
|
187420
|
+
*
|
|
187421
|
+
* **不走**「pending 超时即 failed」那条分支:那条规则会把「占位了但还没产生副作用」的 run 直接
|
|
187422
|
+
* 判死,等于永久漏一次触发。这里按稳定 runId 重试同一下游命令键,有界退避;
|
|
187423
|
+
* 达到重试上限的基础设施错误才落 failed 并告警(该 run 仍占着事件键,不自动制造第二次业务执行)。
|
|
187424
|
+
*/
|
|
187425
|
+
async reconcileEventRun(run) {
|
|
187426
|
+
const attempts = eventAttempts(run);
|
|
187427
|
+
const nowMs = this.now().getTime();
|
|
187428
|
+
const lastAt = eventLastAttemptAt(run) ?? Date.parse(run.triggeredAt);
|
|
187429
|
+
if (attempts >= EVENT_RETRY_MAX_ATTEMPTS) {
|
|
187430
|
+
await this.store.settleRun(run.id, {
|
|
187431
|
+
status: "failed",
|
|
187432
|
+
error: `\u8BA2\u9605\u89E6\u53D1\u6D3E\u53D1\u91CD\u8BD5 ${attempts} \u6B21\u4ECD\u5931\u8D25\uFF1A${eventLastError(run) ?? "\u672A\u77E5\u9519\u8BEF"}`,
|
|
187433
|
+
reasonCode: "spawn_failed",
|
|
187434
|
+
completedAt: this.now().toISOString()
|
|
187435
|
+
});
|
|
187436
|
+
this.log?.(`[automations] \u26A0 \u8BA2\u9605\u89E6\u53D1 run ${run.id} \u8FBE\u91CD\u8BD5\u4E0A\u9650\u5DF2\u843D failed\u2014\u2014\u8BE5 run \u4ECD\u5360\u7740\u4E8B\u4EF6\u952E\uFF0C\u4EBA\u5DE5\u6062\u590D\u5FC5\u987B\u91CD\u8BD5\u540C\u4E00 runId`);
|
|
187437
|
+
return;
|
|
187438
|
+
}
|
|
187439
|
+
const backoffMs = attempts === 0 ? 0 : Math.min(EVENT_RETRY_BASE_MS * 2 ** (attempts - 1), EVENT_RETRY_MAX_MS);
|
|
187440
|
+
if (nowMs - lastAt < backoffMs) return;
|
|
187441
|
+
const automation = await this.store.getAutomation(run.automationId);
|
|
187442
|
+
if (!automation) return;
|
|
187443
|
+
const trigger = run.triggerId ? await this.store.getTrigger(run.triggerId) : null;
|
|
187444
|
+
await this.dispatchEventRun(automation, trigger, run).catch((e) => {
|
|
187445
|
+
this.log?.(`[automations] \u8BA2\u9605\u89E6\u53D1\u91CD\u8BD5\u5F02\u5E38 ${run.id}\uFF1A${String(e)}`);
|
|
187446
|
+
});
|
|
187447
|
+
}
|
|
187448
|
+
async admissionSkip(automation, source = "manual") {
|
|
187198
187449
|
if (automation.archivedAt) return { reasonCode: "automation_archived", error: "\u81EA\u52A8\u5316\u5DF2\u5F52\u6863" };
|
|
187199
187450
|
if (!automation.enabled) return { reasonCode: "automation_paused", error: "\u81EA\u52A8\u5316\u5DF2\u6682\u505C" };
|
|
187200
187451
|
if (automation.executionMode === "chat") {
|
|
187201
187452
|
if (!automation.agentId) return { reasonCode: "config_error", error: "chat \u6A21\u5F0F\u7F3A\u5C11\u6267\u884C\u4EBA\uFF08agentId\uFF09" };
|
|
187202
187453
|
if (!this.deps.runChat) return { reasonCode: "config_error", error: "\u672C\u5B9E\u4F8B\u672A\u63A5 chat \u6267\u884C" };
|
|
187454
|
+
if (source === "event" && !this.deps.chatSupportsIdempotency) {
|
|
187455
|
+
return { reasonCode: "config_error", error: "\u5BF9\u8BDD\u6267\u884C\u6682\u4E0D\u652F\u6301\u8BA2\u9605\u89E6\u53D1\uFF08chat \u9002\u914D\u5668\u7F3A\u6301\u4E45\u5E42\u7B49\u952E\uFF09" };
|
|
187456
|
+
}
|
|
187203
187457
|
if (this.deps.agentReady) {
|
|
187204
187458
|
const ready = await this.deps.agentReady(automation.agentId);
|
|
187205
187459
|
if (!ready.ok) return { reasonCode: ready.reason, error: ready.error };
|
|
@@ -187319,7 +187573,9 @@ var init_service8 = __esm({
|
|
|
187319
187573
|
...human ? { humanActorId: human } : {},
|
|
187320
187574
|
...tagId ? { tags: [tagId] } : {},
|
|
187321
187575
|
// 成本安全(ADR 0010):dispatch=false(默认)→ 建单即设 dispatch-hold,待人放行。
|
|
187322
|
-
...automation.dispatch ? {} : { dispatch: false }
|
|
187576
|
+
...automation.dispatch ? {} : { dispatch: false },
|
|
187577
|
+
// 稳定下游键(ADR 0163 §4.4):重试落在同一张工单上,占位后崩溃不会建出第二张。
|
|
187578
|
+
...opts.commandKey ? { idempotencyKey: opts.commandKey } : {}
|
|
187323
187579
|
});
|
|
187324
187580
|
return { workspace: result.workspace, rootArtifactId: result.rootArtifactId };
|
|
187325
187581
|
}
|
|
@@ -187362,6 +187618,10 @@ var init_service8 = __esm({
|
|
|
187362
187618
|
continue;
|
|
187363
187619
|
}
|
|
187364
187620
|
if (run.status === "pending") {
|
|
187621
|
+
if (run.source === "event") {
|
|
187622
|
+
await this.reconcileEventRun(run);
|
|
187623
|
+
continue;
|
|
187624
|
+
}
|
|
187365
187625
|
if (nowMs - Date.parse(run.triggeredAt) > 10 * 60 * 1e3) {
|
|
187366
187626
|
await this.store.settleRun(run.id, {
|
|
187367
187627
|
status: "failed",
|
|
@@ -187438,6 +187698,15 @@ var init_service8 = __esm({
|
|
|
187438
187698
|
}
|
|
187439
187699
|
}
|
|
187440
187700
|
};
|
|
187701
|
+
EVENT_RETRY_BASE_MS = 3e4;
|
|
187702
|
+
EVENT_RETRY_MAX_MS = 10 * 6e4;
|
|
187703
|
+
EVENT_RETRY_MAX_ATTEMPTS = 5;
|
|
187704
|
+
eventAttempts = (run) => eventDispatchState(run).attempts ?? 0;
|
|
187705
|
+
eventLastAttemptAt = (run) => {
|
|
187706
|
+
const at = eventDispatchState(run).lastAttemptAt;
|
|
187707
|
+
return at ? Date.parse(at) : null;
|
|
187708
|
+
};
|
|
187709
|
+
eventLastError = (run) => eventDispatchState(run).lastError ?? null;
|
|
187441
187710
|
CHAT_REPLY_MAX = 2e4;
|
|
187442
187711
|
}
|
|
187443
187712
|
});
|
|
@@ -187821,7 +188090,8 @@ function buildAutomationSummary(automation, triggers, recent, canWrite) {
|
|
|
187821
188090
|
lastRun,
|
|
187822
188091
|
nextRunAt,
|
|
187823
188092
|
recent: ordered.slice(0, RECENT_LIMIT).map(stripRunPayload),
|
|
187824
|
-
canWrite
|
|
188093
|
+
canWrite,
|
|
188094
|
+
capabilities: { eventTrigger: { supported: true } }
|
|
187825
188095
|
};
|
|
187826
188096
|
}
|
|
187827
188097
|
function deriveNextRunAt(triggers) {
|
|
@@ -187867,10 +188137,12 @@ function validateTriggers(raw) {
|
|
|
187867
188137
|
label: t.label ?? null,
|
|
187868
188138
|
webhook: { provider, ...eventFilters !== void 0 ? { eventFilters } : {} }
|
|
187869
188139
|
});
|
|
188140
|
+
} else if (t?.kind === "event") {
|
|
188141
|
+
out.push({ kind: "event", enabled: t.enabled ?? true, label: t.label ?? null, event: validateEventConfig(t.event) });
|
|
187870
188142
|
} else if (t?.kind === "manual") {
|
|
187871
188143
|
out.push({ kind: "manual", enabled: t.enabled ?? true, label: t.label ?? null });
|
|
187872
188144
|
} else {
|
|
187873
|
-
throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 manual");
|
|
188145
|
+
throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook\u3001event \u6216 manual");
|
|
187874
188146
|
}
|
|
187875
188147
|
}
|
|
187876
188148
|
return out;
|
|
@@ -187892,6 +188164,28 @@ function validateEventFilters(raw) {
|
|
|
187892
188164
|
}
|
|
187893
188165
|
return out;
|
|
187894
188166
|
}
|
|
188167
|
+
function validateEventConfig(raw) {
|
|
188168
|
+
const cfg = raw ?? {};
|
|
188169
|
+
if (cfg.source !== void 0 && cfg.source !== "project") {
|
|
188170
|
+
throw new ApiError(400, "BAD_REQUEST", `\u4E0D\u652F\u6301\u7684\u4E8B\u4EF6\u6E90\uFF1A${String(cfg.source)}\uFF08\u672C\u671F\u53EA\u652F\u6301 project\uFF09`);
|
|
188171
|
+
}
|
|
188172
|
+
const projectId = typeof cfg.projectId === "string" ? cfg.projectId.trim() : "";
|
|
188173
|
+
if (!projectId) throw new ApiError(400, "BAD_REQUEST", "event \u89E6\u53D1\u5668\u9700\u8981 event.projectId");
|
|
188174
|
+
const events = normalizeEventNames(cfg.events);
|
|
188175
|
+
return { source: "project", projectId, events };
|
|
188176
|
+
}
|
|
188177
|
+
function normalizeEventNames(raw) {
|
|
188178
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
188179
|
+
throw new ApiError(400, "BAD_REQUEST", `event.events \u9700\u8981\u975E\u7A7A\u6570\u7EC4\uFF08\u53EF\u9009\u503C\uFF1A${AUTOMATION_EVENT_NAMES.join(", ")}\uFF09`);
|
|
188180
|
+
}
|
|
188181
|
+
const known = new Set(AUTOMATION_EVENT_NAMES);
|
|
188182
|
+
for (const e of raw) {
|
|
188183
|
+
if (typeof e !== "string" || !known.has(e)) {
|
|
188184
|
+
throw new ApiError(400, "UNKNOWN_EVENT", `\u4E0D\u652F\u6301\u7684\u4E8B\u4EF6\u7C7B\u578B\uFF1A${String(e)}\uFF08\u53EF\u9009\u503C\uFF1A${AUTOMATION_EVENT_NAMES.join(", ")}\uFF09`);
|
|
188185
|
+
}
|
|
188186
|
+
}
|
|
188187
|
+
return AUTOMATION_EVENT_NAMES.filter((n) => raw.includes(n));
|
|
188188
|
+
}
|
|
187895
188189
|
function assertValidCron(cron, tz) {
|
|
187896
188190
|
const v2 = validateCron(cron, tz);
|
|
187897
188191
|
if (!v2.ok) throw new ApiError(400, v2.code, v2.message);
|
|
@@ -187954,13 +188248,66 @@ function automationsDomain(opts) {
|
|
|
187954
188248
|
throw new ApiError(403, "NOT_ALLOWED", "\u53EA\u6709\u521B\u5EFA\u8005\u6216\u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u534F\u4F5C\u8005");
|
|
187955
188249
|
}
|
|
187956
188250
|
};
|
|
188251
|
+
const decorateEventTriggers = async (automation, triggers, req) => {
|
|
188252
|
+
if (!triggers.some((t) => t.kind === "event")) return triggers;
|
|
188253
|
+
const cache = /* @__PURE__ */ new Map();
|
|
188254
|
+
const out = [];
|
|
188255
|
+
for (const t of triggers) {
|
|
188256
|
+
if (t.kind !== "event" || !t.event) {
|
|
188257
|
+
out.push(t);
|
|
188258
|
+
continue;
|
|
188259
|
+
}
|
|
188260
|
+
const pid = t.event.projectId;
|
|
188261
|
+
if (!cache.has(pid)) {
|
|
188262
|
+
cache.set(pid, await opts.projectAccess?.({ companyId: companyOf(req), projectId: pid, actor: req.auth.actor }) ?? null);
|
|
188263
|
+
}
|
|
188264
|
+
const project = cache.get(pid) ?? null;
|
|
188265
|
+
out.push({
|
|
188266
|
+
...t,
|
|
188267
|
+
event: { ...t.event, projectAvailable: project !== null, projectName: project?.name ?? null }
|
|
188268
|
+
});
|
|
188269
|
+
}
|
|
188270
|
+
return out;
|
|
188271
|
+
};
|
|
187957
188272
|
const summarize3 = async (automation, req) => {
|
|
187958
188273
|
const [triggers, recent, canWrite] = await Promise.all([
|
|
187959
188274
|
service.listTriggers(automation.id),
|
|
187960
188275
|
service.listRuns(automation.id, { limit: RECENT_LIMIT2 }),
|
|
187961
188276
|
canWriteOf(automation, req)
|
|
187962
188277
|
]);
|
|
187963
|
-
|
|
188278
|
+
const decorated = await decorateEventTriggers(automation, triggers, req);
|
|
188279
|
+
return {
|
|
188280
|
+
...buildAutomationSummary(automation, decorated, recent, canWrite),
|
|
188281
|
+
// 能力位服务端算(ADR 0163 §10):前端只消费不推断,否则前后端两套规则必漂移。
|
|
188282
|
+
capabilities: { eventTrigger: service.eventTriggerCapability(automation) }
|
|
188283
|
+
};
|
|
188284
|
+
};
|
|
188285
|
+
const assertEventTriggerWritable = async (mode, cfg, existing, req) => {
|
|
188286
|
+
if (!service.eventTriggerCapability({ executionMode: mode }).supported) {
|
|
188287
|
+
throw new ApiError(400, "EVENT_TRIGGER_UNSUPPORTED", "\u8BE5\u6267\u884C\u65B9\u5F0F\u6682\u4E0D\u652F\u6301\u8BA2\u9605\u89E6\u53D1\uFF08\u5BF9\u8BDD\u6267\u884C\u7F3A\u6301\u4E45\u5E42\u7B49\u952E\uFF09");
|
|
188288
|
+
}
|
|
188289
|
+
const project = await opts.projectAccess?.({ companyId: companyOf(req), projectId: cfg.projectId, actor: req.auth.actor });
|
|
188290
|
+
if (!project) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9879\u76EE\uFF1A${cfg.projectId}`);
|
|
188291
|
+
for (const other of existing) {
|
|
188292
|
+
if (other.projectId !== cfg.projectId) continue;
|
|
188293
|
+
const dup = cfg.events.find((e) => other.events.includes(e));
|
|
188294
|
+
if (dup) throw new ApiError(409, "DUPLICATE_SUBSCRIPTION", `\u8BE5\u9879\u76EE\u7684\u8FD9\u4E2A\u4E8B\u4EF6\u5DF2\u8BA2\u9605\uFF1A${dup}`);
|
|
188295
|
+
}
|
|
188296
|
+
};
|
|
188297
|
+
const assertEventTriggerInputs = async (mode, inputs, kept, req) => {
|
|
188298
|
+
const seen = [...kept];
|
|
188299
|
+
for (const input of inputs ?? []) {
|
|
188300
|
+
if (input.kind !== "event" || !input.event) continue;
|
|
188301
|
+
await assertEventTriggerWritable(mode, input.event, seen, req);
|
|
188302
|
+
seen.push(input.event);
|
|
188303
|
+
}
|
|
188304
|
+
};
|
|
188305
|
+
const assertModeKeepsEventTriggers = async (automation, effMode) => {
|
|
188306
|
+
if (service.eventTriggerCapability({ ...automation, executionMode: effMode }).supported) return;
|
|
188307
|
+
const hasEvent = (await service.listTriggers(automation.id)).some((t) => t.kind === "event");
|
|
188308
|
+
if (hasEvent) {
|
|
188309
|
+
throw new ApiError(400, "EVENT_TRIGGER_UNSUPPORTED", "\u8FD9\u6761\u81EA\u52A8\u5316\u6709\u8BA2\u9605\u89E6\u53D1\u5668\uFF0C\u6539\u6210\u5BF9\u8BDD\u6267\u884C\u4F1A\u5931\u53BB\u6301\u4E45\u5E42\u7B49\u4FDD\u8BC1\u2014\u2014\u8BF7\u5148\u5220\u9664\u8BA2\u9605\u89E6\u53D1\u5668");
|
|
188310
|
+
}
|
|
187964
188311
|
};
|
|
187965
188312
|
return (router) => {
|
|
187966
188313
|
router.get("/api/automations/templates", async () => {
|
|
@@ -188027,6 +188374,7 @@ function automationsDomain(opts) {
|
|
|
188027
188374
|
assertTitleTemplate(b2.titleTemplate);
|
|
188028
188375
|
const subscribers = assertActorIds(b2.subscribers, "subscribers");
|
|
188029
188376
|
const triggers = validateTriggers(b2.triggers);
|
|
188377
|
+
await assertEventTriggerInputs(mode, triggers, [], req);
|
|
188030
188378
|
const created = await service.createAutomation({
|
|
188031
188379
|
companyId: companyOf(req),
|
|
188032
188380
|
name: b2.name,
|
|
@@ -188060,6 +188408,9 @@ function automationsDomain(opts) {
|
|
|
188060
188408
|
assertTitleTemplate(b2.titleTemplate);
|
|
188061
188409
|
const subscribers = assertActorIds(b2.subscribers, "subscribers");
|
|
188062
188410
|
const triggers = validateTriggers(b2.triggers);
|
|
188411
|
+
const kept = triggers !== void 0 ? [] : (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
|
|
188412
|
+
await assertEventTriggerInputs(effMode, triggers, kept, req);
|
|
188413
|
+
if (triggers === void 0) await assertModeKeepsEventTriggers(automation, effMode);
|
|
188063
188414
|
const updated = await service.updateAutomation(
|
|
188064
188415
|
automation.id,
|
|
188065
188416
|
{
|
|
@@ -188108,6 +188459,8 @@ function automationsDomain(opts) {
|
|
|
188108
188459
|
const automation = await mustGet(req);
|
|
188109
188460
|
await requireWrite(automation, req);
|
|
188110
188461
|
const inputs = validateTriggers([req.body]) ?? [];
|
|
188462
|
+
const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
|
|
188463
|
+
await assertEventTriggerInputs(automation.executionMode, inputs, kept, req);
|
|
188111
188464
|
const trigger = await service.addTrigger(automation.id, inputs[0], req.auth.actor);
|
|
188112
188465
|
return { status: 201, body: trigger };
|
|
188113
188466
|
});
|
|
@@ -188137,6 +188490,17 @@ function automationsDomain(opts) {
|
|
|
188137
188490
|
const eventFilters = validateEventFilters(b2.webhook.eventFilters);
|
|
188138
188491
|
if (eventFilters !== void 0) patch.webhook = { eventFilters };
|
|
188139
188492
|
}
|
|
188493
|
+
if (b2.event !== void 0) {
|
|
188494
|
+
if (existing.kind !== "event") throw new ApiError(400, "BAD_REQUEST", "\u975E event \u89E6\u53D1\u5668\u4E0D\u63A5\u53D7 event \u5B57\u6BB5");
|
|
188495
|
+
const raw = b2.event;
|
|
188496
|
+
if (raw["projectId"] !== void 0 || raw["source"] !== void 0) {
|
|
188497
|
+
throw new ApiError(400, "BAD_REQUEST", "\u8BA2\u9605\u7684\u9879\u76EE\u4E0E\u4E8B\u4EF6\u6E90\u4E0D\u53EF\u4FEE\u6539\u2014\u2014\u8BF7\u5220\u9664\u8FD9\u6761\u89E6\u53D1\u5668\u540E\u91CD\u65B0\u6DFB\u52A0");
|
|
188498
|
+
}
|
|
188499
|
+
const events = normalizeEventNames(raw["events"]);
|
|
188500
|
+
const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.id !== existing.id && t.event).map((t) => t.event);
|
|
188501
|
+
await assertEventTriggerWritable(automation.executionMode, { projectId: existing.event.projectId, events }, kept, req);
|
|
188502
|
+
patch.event = { events };
|
|
188503
|
+
}
|
|
188140
188504
|
const updated = await service.updateTrigger(existing.id, patch, req.auth.actor);
|
|
188141
188505
|
return { status: 200, body: updated };
|
|
188142
188506
|
});
|
|
@@ -188218,6 +188582,7 @@ var RECENT_LIMIT2;
|
|
|
188218
188582
|
var init_routes9 = __esm({
|
|
188219
188583
|
"../server/src/domains/automations/routes.ts"() {
|
|
188220
188584
|
"use strict";
|
|
188585
|
+
init_src2();
|
|
188221
188586
|
init_templates();
|
|
188222
188587
|
init_router();
|
|
188223
188588
|
init_service8();
|
|
@@ -188230,17 +188595,25 @@ var init_routes9 = __esm({
|
|
|
188230
188595
|
});
|
|
188231
188596
|
|
|
188232
188597
|
// ../server/src/domains/automations/workorder-state.ts
|
|
188233
|
-
function
|
|
188598
|
+
function makeAutomationWorkorderSnapshotResolver(kernel) {
|
|
188234
188599
|
return async (workspace) => {
|
|
188235
188600
|
const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
|
|
188236
|
-
if (arts.length === 0) return "unknown";
|
|
188601
|
+
if (arts.length === 0) return { state: "unknown", members: [], rootArtifactId: null };
|
|
188237
188602
|
const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
|
|
188238
|
-
|
|
188603
|
+
const members = arts.map((a) => ({ artifactId: String(a.id), currentRevisionId: a.currentRev ? String(a.currentRev) : null })).sort((x2, y) => x2.artifactId < y.artifactId ? -1 : x2.artifactId > y.artifactId ? 1 : 0);
|
|
188604
|
+
const rootArtifactId = String(seed.id);
|
|
188605
|
+
if (lifecycleOf(kernel.model, seed.id) === "sealed:cancelled") {
|
|
188606
|
+
return { state: "cancelled", members, rootArtifactId };
|
|
188607
|
+
}
|
|
188239
188608
|
const stage = deriveStage(arts.map((a) => nodeInfo(kernel.model, a)));
|
|
188240
|
-
if (stage === "concluded" || stage === "sealed") return "done";
|
|
188241
|
-
return "active";
|
|
188609
|
+
if (stage === "concluded" || stage === "sealed") return { state: "done", members, rootArtifactId };
|
|
188610
|
+
return { state: "active", members, rootArtifactId };
|
|
188242
188611
|
};
|
|
188243
188612
|
}
|
|
188613
|
+
function makeAutomationWorkorderStateResolver(kernel) {
|
|
188614
|
+
const snapshot = makeAutomationWorkorderSnapshotResolver(kernel);
|
|
188615
|
+
return async (workspace) => (await snapshot(workspace)).state;
|
|
188616
|
+
}
|
|
188244
188617
|
function makeAutomationReplyResolver(kernel, blobs) {
|
|
188245
188618
|
return async (workspace) => {
|
|
188246
188619
|
const model = kernel.model;
|
|
@@ -188304,7 +188677,12 @@ function createAutomationsDomain(opts) {
|
|
|
188304
188677
|
return {
|
|
188305
188678
|
service,
|
|
188306
188679
|
webhook,
|
|
188307
|
-
register: automationsDomain({
|
|
188680
|
+
register: automationsDomain({
|
|
188681
|
+
service,
|
|
188682
|
+
webhook,
|
|
188683
|
+
...opts.isAdmin ? { isAdmin: opts.isAdmin } : {},
|
|
188684
|
+
...opts.projectAccess ? { projectAccess: opts.projectAccess } : {}
|
|
188685
|
+
})
|
|
188308
188686
|
};
|
|
188309
188687
|
}
|
|
188310
188688
|
var init_automations = __esm({
|
|
@@ -188596,6 +188974,210 @@ var init_playbooks = __esm({
|
|
|
188596
188974
|
}
|
|
188597
188975
|
});
|
|
188598
188976
|
|
|
188977
|
+
// ../server/src/automations/project-event-poller.ts
|
|
188978
|
+
function completionFingerprint(workspace, members) {
|
|
188979
|
+
const canonical = JSON.stringify({
|
|
188980
|
+
workspace,
|
|
188981
|
+
members: [...members].sort((a, b2) => a.artifactId < b2.artifactId ? -1 : a.artifactId > b2.artifactId ? 1 : 0).map((m2) => ({ artifactId: m2.artifactId, currentRevisionId: m2.currentRevisionId ?? null }))
|
|
188982
|
+
});
|
|
188983
|
+
return (0, import_node_crypto39.createHash)("sha256").update(canonical).digest("hex");
|
|
188984
|
+
}
|
|
188985
|
+
function eventConsumptionKey(args) {
|
|
188986
|
+
const raw = `project-event:${EVENT_KEY_VERSION}:${args.triggerId}:${args.event}:${args.workspace}:${args.fingerprint}`;
|
|
188987
|
+
return raw.length <= 200 ? raw : `project-event:${EVENT_KEY_VERSION}:sha256:${(0, import_node_crypto39.createHash)("sha256").update(raw).digest("hex")}`;
|
|
188988
|
+
}
|
|
188989
|
+
var import_node_crypto39, EVENT_KEY_VERSION, EMPTY_METRICS, AUTOMATION_EVENT_HANDLERS, ProjectEventPoller;
|
|
188990
|
+
var init_project_event_poller = __esm({
|
|
188991
|
+
"../server/src/automations/project-event-poller.ts"() {
|
|
188992
|
+
"use strict";
|
|
188993
|
+
import_node_crypto39 = require("node:crypto");
|
|
188994
|
+
EVENT_KEY_VERSION = "v1";
|
|
188995
|
+
EMPTY_METRICS = () => ({
|
|
188996
|
+
scanDurationMs: 0,
|
|
188997
|
+
projectsScanned: 0,
|
|
188998
|
+
workordersScanned: 0,
|
|
188999
|
+
newEvents: 0,
|
|
189000
|
+
duplicateEvents: 0,
|
|
189001
|
+
skippedEvents: 0,
|
|
189002
|
+
baselineEvents: 0,
|
|
189003
|
+
pollErrors: 0,
|
|
189004
|
+
oldestPendingAgeMs: null
|
|
189005
|
+
});
|
|
189006
|
+
AUTOMATION_EVENT_HANDLERS = {
|
|
189007
|
+
// 完成判据必须来自 D3 resolver(snapshot.state),不在这里复制 deriveStage。
|
|
189008
|
+
// active / unknown 是「还没完成」,cancelled 永不产生 completed——四态里只有 done 触发。
|
|
189009
|
+
"workorder.completed": {
|
|
189010
|
+
matches: (snapshot) => snapshot.state === "done",
|
|
189011
|
+
fingerprint: (workspace, snapshot) => completionFingerprint(workspace, snapshot.members)
|
|
189012
|
+
}
|
|
189013
|
+
};
|
|
189014
|
+
ProjectEventPoller = class {
|
|
189015
|
+
deps;
|
|
189016
|
+
now;
|
|
189017
|
+
log;
|
|
189018
|
+
batchLimit;
|
|
189019
|
+
inFlight = false;
|
|
189020
|
+
lastMetrics = EMPTY_METRICS();
|
|
189021
|
+
constructor(deps) {
|
|
189022
|
+
this.deps = deps;
|
|
189023
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
189024
|
+
if (deps.log) this.log = deps.log;
|
|
189025
|
+
this.batchLimit = deps.batchLimit ?? 200;
|
|
189026
|
+
}
|
|
189027
|
+
metrics() {
|
|
189028
|
+
return { ...this.lastMetrics };
|
|
189029
|
+
}
|
|
189030
|
+
/** 一轮扫描。单实例用 inFlight 防重入;多实例无需选主——并发由数据库唯一键裁决。 */
|
|
189031
|
+
async tick() {
|
|
189032
|
+
if (this.inFlight) return this.lastMetrics;
|
|
189033
|
+
this.inFlight = true;
|
|
189034
|
+
const startedAt = this.now().getTime();
|
|
189035
|
+
const m2 = EMPTY_METRICS();
|
|
189036
|
+
try {
|
|
189037
|
+
const triggers = await this.deps.store.listEventTriggers();
|
|
189038
|
+
if (triggers.length === 0) return this.lastMetrics = { ...m2, scanDurationMs: this.now().getTime() - startedAt };
|
|
189039
|
+
const snapshots2 = /* @__PURE__ */ new Map();
|
|
189040
|
+
let budget = this.batchLimit;
|
|
189041
|
+
for (const trigger of triggers) {
|
|
189042
|
+
if (budget <= 0) break;
|
|
189043
|
+
const config2 = trigger.event;
|
|
189044
|
+
if (!config2?.projectId || config2.events.length === 0) continue;
|
|
189045
|
+
const automation = await this.deps.store.getAutomation(trigger.automationId);
|
|
189046
|
+
if (!automation) continue;
|
|
189047
|
+
const groupKey = `${automation.companyId}\0${config2.projectId}`;
|
|
189048
|
+
let group = snapshots2.get(groupKey);
|
|
189049
|
+
if (!group) {
|
|
189050
|
+
try {
|
|
189051
|
+
group = await this.scanProject(automation.companyId, config2.projectId);
|
|
189052
|
+
m2.projectsScanned += 1;
|
|
189053
|
+
m2.workordersScanned += group.length;
|
|
189054
|
+
} catch (e) {
|
|
189055
|
+
m2.pollErrors += 1;
|
|
189056
|
+
this.log?.(`[project-event-poller] \u626B\u63CF\u9879\u76EE\u5931\u8D25 ${config2.projectId}\uFF1A${String(e)}`);
|
|
189057
|
+
group = [];
|
|
189058
|
+
}
|
|
189059
|
+
snapshots2.set(groupKey, group);
|
|
189060
|
+
}
|
|
189061
|
+
try {
|
|
189062
|
+
budget -= await this.evaluateTrigger(automation, trigger, group, m2, budget);
|
|
189063
|
+
} catch (e) {
|
|
189064
|
+
m2.pollErrors += 1;
|
|
189065
|
+
this.log?.(`[project-event-poller] \u89E6\u53D1\u5668\u6C42\u503C\u5931\u8D25 ${trigger.id}\uFF1A${String(e)}`);
|
|
189066
|
+
}
|
|
189067
|
+
}
|
|
189068
|
+
m2.oldestPendingAgeMs = await this.oldestPendingAge();
|
|
189069
|
+
} catch (e) {
|
|
189070
|
+
m2.pollErrors += 1;
|
|
189071
|
+
this.log?.(`[project-event-poller] tick \u5931\u8D25\uFF1A${String(e)}`);
|
|
189072
|
+
} finally {
|
|
189073
|
+
m2.scanDurationMs = this.now().getTime() - startedAt;
|
|
189074
|
+
this.lastMetrics = m2;
|
|
189075
|
+
this.inFlight = false;
|
|
189076
|
+
}
|
|
189077
|
+
if (m2.newEvents || m2.skippedEvents || m2.baselineEvents || m2.pollErrors) {
|
|
189078
|
+
this.log?.(
|
|
189079
|
+
`[project-event-poller] \u9879\u76EE ${m2.projectsScanned} / \u5DE5\u5355 ${m2.workordersScanned} \u2192 \u65B0\u4E8B\u4EF6 ${m2.newEvents}\u3001\u91CD\u590D ${m2.duplicateEvents}\u3001\u8DF3\u8FC7 ${m2.skippedEvents}\u3001\u57FA\u7EBF ${m2.baselineEvents}\u3001\u9519\u8BEF ${m2.pollErrors}\uFF08${m2.scanDurationMs}ms\uFF09`
|
|
189080
|
+
);
|
|
189081
|
+
}
|
|
189082
|
+
return this.lastMetrics;
|
|
189083
|
+
}
|
|
189084
|
+
async scanProject(companyId, projectId) {
|
|
189085
|
+
const workspaces = await this.deps.listProjectWorkspaces({ companyId, projectId });
|
|
189086
|
+
const out = [];
|
|
189087
|
+
for (const workspace of workspaces) {
|
|
189088
|
+
out.push({ workspace, snapshot: await this.deps.workorderSnapshot({ companyId, workspace }) });
|
|
189089
|
+
}
|
|
189090
|
+
return out;
|
|
189091
|
+
}
|
|
189092
|
+
/** 返回本触发器消耗掉的预算条数。 */
|
|
189093
|
+
async evaluateTrigger(automation, trigger, candidates, m2, budget) {
|
|
189094
|
+
const config2 = trigger.event;
|
|
189095
|
+
const baselining = config2.baselineState !== "ready";
|
|
189096
|
+
let used = 0;
|
|
189097
|
+
for (const event of config2.events) {
|
|
189098
|
+
const handler = AUTOMATION_EVENT_HANDLERS[event];
|
|
189099
|
+
if (!handler) continue;
|
|
189100
|
+
for (const { workspace, snapshot } of candidates) {
|
|
189101
|
+
if (!handler.matches(snapshot)) continue;
|
|
189102
|
+
if (used >= budget) return used;
|
|
189103
|
+
const fingerprint = handler.fingerprint(workspace, snapshot);
|
|
189104
|
+
const key = eventConsumptionKey({ triggerId: trigger.id, event, workspace, fingerprint });
|
|
189105
|
+
const envelope = {
|
|
189106
|
+
event,
|
|
189107
|
+
companyId: automation.companyId,
|
|
189108
|
+
projectId: config2.projectId,
|
|
189109
|
+
workspace,
|
|
189110
|
+
rootArtifactId: snapshot.rootArtifactId,
|
|
189111
|
+
completionFingerprint: fingerprint,
|
|
189112
|
+
observedAt: this.now().toISOString(),
|
|
189113
|
+
...await this.originAutomationOf(workspace)
|
|
189114
|
+
};
|
|
189115
|
+
used += 1;
|
|
189116
|
+
if (baselining) {
|
|
189117
|
+
const { created: created2 } = await this.deps.service.claimEventRun(
|
|
189118
|
+
automation,
|
|
189119
|
+
trigger,
|
|
189120
|
+
key,
|
|
189121
|
+
envelope,
|
|
189122
|
+
"skipped",
|
|
189123
|
+
"subscription_baseline",
|
|
189124
|
+
"\u8BA2\u9605\u57FA\u7EBF\u5BF9\u9F50\uFF1A\u8BA2\u9605\u751F\u6548\u524D\u5DF2\u5B8C\u6210\uFF0C\u4E0D\u56DE\u653E\u5386\u53F2"
|
|
189125
|
+
);
|
|
189126
|
+
if (created2) m2.baselineEvents += 1;
|
|
189127
|
+
else m2.duplicateEvents += 1;
|
|
189128
|
+
continue;
|
|
189129
|
+
}
|
|
189130
|
+
const { run, created } = await this.deps.service.claimEventRun(automation, trigger, key, envelope);
|
|
189131
|
+
if (!created) {
|
|
189132
|
+
m2.duplicateEvents += 1;
|
|
189133
|
+
continue;
|
|
189134
|
+
}
|
|
189135
|
+
const denial = await this.admit(automation, trigger, envelope);
|
|
189136
|
+
if (denial) {
|
|
189137
|
+
await this.deps.service.skipEventRun(run, denial.reasonCode, denial.error);
|
|
189138
|
+
m2.skippedEvents += 1;
|
|
189139
|
+
continue;
|
|
189140
|
+
}
|
|
189141
|
+
await this.deps.service.dispatchEventRun(automation, trigger, run);
|
|
189142
|
+
m2.newEvents += 1;
|
|
189143
|
+
}
|
|
189144
|
+
}
|
|
189145
|
+
if (baselining) {
|
|
189146
|
+
await this.deps.store.completeEventBaseline(trigger.id, this.now().toISOString());
|
|
189147
|
+
}
|
|
189148
|
+
return used;
|
|
189149
|
+
}
|
|
189150
|
+
/** 权限复查 + 自环准入。两条都只在**占位成功之后**做,因为拒绝也要留痕在同一把键上。 */
|
|
189151
|
+
async admit(automation, trigger, envelope) {
|
|
189152
|
+
if (envelope.originAutomationId && envelope.originAutomationId === automation.id) {
|
|
189153
|
+
return { reasonCode: "self_trigger_blocked", error: "\u81EA\u89E6\u53D1\u5DF2\u963B\u65AD\uFF1A\u8FD9\u4E2A\u5DE5\u5355\u662F\u672C\u81EA\u52A8\u5316\u81EA\u5DF1\u5EFA\u7684" };
|
|
189154
|
+
}
|
|
189155
|
+
const accountable = trigger.publishedBy ?? (await this.deps.store.latestRuleVersion(automation.id))?.publishedBy ?? automation.createdBy ?? null;
|
|
189156
|
+
const readable = await this.deps.projectReadable({
|
|
189157
|
+
companyId: automation.companyId,
|
|
189158
|
+
projectId: envelope.projectId,
|
|
189159
|
+
actor: accountable
|
|
189160
|
+
});
|
|
189161
|
+
if (!readable) {
|
|
189162
|
+
return { reasonCode: "project_access_revoked", error: `\u8BA2\u9605\u9879\u76EE\u5DF2\u4E0D\u53EF\u8BBF\u95EE\uFF1A${envelope.projectId}` };
|
|
189163
|
+
}
|
|
189164
|
+
return null;
|
|
189165
|
+
}
|
|
189166
|
+
async originAutomationOf(workspace) {
|
|
189167
|
+
const origin = await this.deps.store.findRunBySpawnedWorkspace(workspace).catch(() => null);
|
|
189168
|
+
return origin ? { originAutomationId: origin.automationId } : {};
|
|
189169
|
+
}
|
|
189170
|
+
async oldestPendingAge() {
|
|
189171
|
+
const unsettled = await this.deps.store.listUnsettledRuns(50).catch(() => []);
|
|
189172
|
+
const pending = unsettled.filter((r) => r.source === "event" && r.status === "pending");
|
|
189173
|
+
if (pending.length === 0) return null;
|
|
189174
|
+
const oldest = Math.min(...pending.map((r) => Date.parse(r.triggeredAt)));
|
|
189175
|
+
return this.now().getTime() - oldest;
|
|
189176
|
+
}
|
|
189177
|
+
};
|
|
189178
|
+
}
|
|
189179
|
+
});
|
|
189180
|
+
|
|
188599
189181
|
// ../server/src/daemon-adapter.ts
|
|
188600
189182
|
function graceFromEnv(fallback) {
|
|
188601
189183
|
const v2 = process.env["OASIS_NODE_LOST_GRACE_MS"];
|
|
@@ -188623,11 +189205,11 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
|
|
|
188623
189205
|
entry.appendWaiters.push({ resolve: resolve10, timer });
|
|
188624
189206
|
});
|
|
188625
189207
|
}
|
|
188626
|
-
var
|
|
189208
|
+
var import_node_crypto40, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
|
|
188627
189209
|
var init_daemon_adapter = __esm({
|
|
188628
189210
|
"../server/src/daemon-adapter.ts"() {
|
|
188629
189211
|
"use strict";
|
|
188630
|
-
|
|
189212
|
+
import_node_crypto40 = require("node:crypto");
|
|
188631
189213
|
STASH_TTL_MS = 5 * 6e4;
|
|
188632
189214
|
STASH_MAX_FRAMES_PER_ID = 500;
|
|
188633
189215
|
STASH_MAX_IDS = 50;
|
|
@@ -188823,7 +189405,7 @@ var init_daemon_adapter = __esm({
|
|
|
188823
189405
|
async spawn(job) {
|
|
188824
189406
|
const nodeId = job.binding?.nodeId;
|
|
188825
189407
|
if (!nodeId) throw new Error("DaemonHubAdapter \u9700\u8981 job.binding.nodeId\uFF08\u8DEF\u7531\u9519\u8BEF\uFF09");
|
|
188826
|
-
const dispatchId = job.dispatchId ?? `dispatch:${(0,
|
|
189408
|
+
const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
|
|
188827
189409
|
const entry = {
|
|
188828
189410
|
nodeId,
|
|
188829
189411
|
...job.binding?.runtimeKind ? { runtimeKind: job.binding.runtimeKind } : {},
|
|
@@ -188979,7 +189561,7 @@ var init_daemon_adapter = __esm({
|
|
|
188979
189561
|
}));
|
|
188980
189562
|
}
|
|
188981
189563
|
request(nodeId, buildFrame) {
|
|
188982
|
-
const requestId = (0,
|
|
189564
|
+
const requestId = (0, import_node_crypto40.randomUUID)();
|
|
188983
189565
|
const sent = this.hub.dispatch(nodeId, buildFrame(requestId));
|
|
188984
189566
|
if (!sent) return Promise.resolve({ ok: false, code: "NODE_OFFLINE" });
|
|
188985
189567
|
return new Promise((resolve10) => {
|
|
@@ -189087,13 +189669,13 @@ function classifyPage(page, lastPushedHash, serviceAccount) {
|
|
|
189087
189669
|
if (sha(body) === lastPushedHash) return { kind: "echo" };
|
|
189088
189670
|
return { kind: "human-edit", content: body, updatedBy: page.updatedBy };
|
|
189089
189671
|
}
|
|
189090
|
-
var
|
|
189672
|
+
var import_node_crypto41, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
|
|
189091
189673
|
var init_engine = __esm({
|
|
189092
189674
|
"../server/src/mirror/engine.ts"() {
|
|
189093
189675
|
"use strict";
|
|
189094
|
-
|
|
189676
|
+
import_node_crypto41 = require("node:crypto");
|
|
189095
189677
|
init_src2();
|
|
189096
|
-
sha = (s2) => (0,
|
|
189678
|
+
sha = (s2) => (0, import_node_crypto41.createHash)("sha256").update(s2, "utf8").digest("hex");
|
|
189097
189679
|
enc3 = (s2) => new TextEncoder().encode(s2);
|
|
189098
189680
|
dec = (b2) => new TextDecoder().decode(b2);
|
|
189099
189681
|
MirrorEngine = class {
|
|
@@ -189300,11 +189882,11 @@ function humanExitCause(exit) {
|
|
|
189300
189882
|
const base = (exit.reason ? label[exit.reason] : void 0) ?? `\u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F\uFF08${exit.reason ?? "\u65E0\u9000\u51FA\u4FE1\u606F"}\uFF09`;
|
|
189301
189883
|
return exit.errorMessage ? `${base}\uFF1A${exit.errorMessage.slice(0, 200)}` : base;
|
|
189302
189884
|
}
|
|
189303
|
-
var
|
|
189885
|
+
var import_node_crypto42, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
|
|
189304
189886
|
var init_worker = __esm({
|
|
189305
189887
|
"../server/src/coordinator/worker.ts"() {
|
|
189306
189888
|
"use strict";
|
|
189307
|
-
|
|
189889
|
+
import_node_crypto42 = require("node:crypto");
|
|
189308
189890
|
init_src5();
|
|
189309
189891
|
init_identity();
|
|
189310
189892
|
AUTONOMOUS_ETHOS = `\u4F60\u662F\u8FD9\u4E2A\u5DE5\u5355\u7684**\u7BA1\u7406\u8005**\u2014\u2014\u804C\u8D23\u662F\u8BA9\u5B83\u987A\u7545\u8DD1\u5B8C\u3002\u7CFB\u7EDF\u5728\u67D0\u4E2A\u8282\u70B9\u5361\u4F4F\u3001\u673A\u68B0\u5206\u8BCA\u786E\u8BA4"\u9700\u8981\u4F60"\u65F6\u5524\u8D77\u4F60\uFF08\u65E0\u4EBA\u5728\u573A\uFF0C\u4F60\u8FD9\u4E00\u8F6E\u628A\u80FD\u505A\u7684\u505A\u6389\uFF09\u3002\u4F60\u7684\u624B\u6BB5\u5F88\u5BBD\uFF1A
|
|
@@ -189551,7 +190133,7 @@ ${HIGH_RISK_COMMANDS}`;
|
|
|
189551
190133
|
return this.deps.kernel.model.lastSeq.get(artifactId) ?? 0;
|
|
189552
190134
|
}
|
|
189553
190135
|
evaluationKey(kind, artifactId, evidence) {
|
|
189554
|
-
const fingerprint = (0,
|
|
190136
|
+
const fingerprint = (0, import_node_crypto42.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
|
|
189555
190137
|
return `${kind}:${artifactId}:${fingerprint}`;
|
|
189556
190138
|
}
|
|
189557
190139
|
artifactEvidence(id) {
|
|
@@ -189928,8 +190510,8 @@ ${ctx.nodeFault}
|
|
|
189928
190510
|
this.deps.log?.(`[coordinator] ${workspace} \u5168\u5C40\u4E0A\u4E0B\u6587\u8BFB\u53D6\u5931\u8D25\uFF0C\u7EE7\u7EED\u6CBF\u7528\u539F\u534F\u8C03\u8005\u4EFB\u52A1\uFF1A${String(error2)}`);
|
|
189929
190511
|
}
|
|
189930
190512
|
}
|
|
189931
|
-
const runtimeSessionId = opts?.resumeSessionId ?? (0,
|
|
189932
|
-
const coordinatorRunId = `coordinate:${(0,
|
|
190513
|
+
const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto42.randomUUID)();
|
|
190514
|
+
const coordinatorRunId = `coordinate:${(0, import_node_crypto42.randomUUID)()}`;
|
|
189933
190515
|
const taskWithContext = coordinatorContext ? [
|
|
189934
190516
|
task,
|
|
189935
190517
|
``,
|
|
@@ -189970,7 +190552,7 @@ ${ctx.nodeFault}
|
|
|
189970
190552
|
const startedAtMs = Date.now();
|
|
189971
190553
|
let trajectoryStarted = false;
|
|
189972
190554
|
if (this.deps.trajectory) {
|
|
189973
|
-
const bundleManifest = Object.fromEntries(Object.entries(job.bundle.files).map(([name, content]) => [name, (0,
|
|
190555
|
+
const bundleManifest = Object.fromEntries(Object.entries(job.bundle.files).map(([name, content]) => [name, (0, import_node_crypto42.createHash)("sha256").update(content).digest("hex")]));
|
|
189974
190556
|
const session = {
|
|
189975
190557
|
runId: coordinatorRunId,
|
|
189976
190558
|
runtimeSessionId,
|
|
@@ -190371,6 +190953,7 @@ var init_src10 = __esm({
|
|
|
190371
190953
|
init_playbooks();
|
|
190372
190954
|
init_overrides();
|
|
190373
190955
|
init_scheduler();
|
|
190956
|
+
init_project_event_poller();
|
|
190374
190957
|
init_daemon_adapter();
|
|
190375
190958
|
init_dispatch_fence();
|
|
190376
190959
|
init_node_health();
|
|
@@ -190391,11 +190974,11 @@ var init_src10 = __esm({
|
|
|
190391
190974
|
});
|
|
190392
190975
|
|
|
190393
190976
|
// ../storage/src/postgres.ts
|
|
190394
|
-
var
|
|
190977
|
+
var import_node_crypto43, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
|
|
190395
190978
|
var init_postgres = __esm({
|
|
190396
190979
|
"../storage/src/postgres.ts"() {
|
|
190397
190980
|
"use strict";
|
|
190398
|
-
|
|
190981
|
+
import_node_crypto43 = require("node:crypto");
|
|
190399
190982
|
init_esm();
|
|
190400
190983
|
init_src2();
|
|
190401
190984
|
ident3 = (s2) => {
|
|
@@ -190574,7 +191157,7 @@ var init_postgres = __esm({
|
|
|
190574
191157
|
return new _PostgresBlobStore(pool, schema);
|
|
190575
191158
|
}
|
|
190576
191159
|
async put(bytes) {
|
|
190577
|
-
const hash = (0,
|
|
191160
|
+
const hash = (0, import_node_crypto43.createHash)("sha256").update(bytes).digest("hex");
|
|
190578
191161
|
await this.pool.query(
|
|
190579
191162
|
`INSERT INTO ${this.t} (hash, bytes, size, content_type) VALUES ($1, $2, $3, $4) ON CONFLICT (hash) DO NOTHING`,
|
|
190580
191163
|
[hash, Buffer.from(bytes), bytes.byteLength, sniffContentType(bytes) ?? null]
|
|
@@ -194843,6 +195426,14 @@ var init_postgres_trace = __esm({
|
|
|
194843
195426
|
});
|
|
194844
195427
|
|
|
194845
195428
|
// ../storage/src/postgres-automations.ts
|
|
195429
|
+
function storedEventConfig(t) {
|
|
195430
|
+
return {
|
|
195431
|
+
source: "project",
|
|
195432
|
+
projectId: t.event?.projectId ?? "",
|
|
195433
|
+
events: t.event?.events ?? [],
|
|
195434
|
+
baselineAt: null
|
|
195435
|
+
};
|
|
195436
|
+
}
|
|
194846
195437
|
function buildTrigger(automationId, input) {
|
|
194847
195438
|
const t = {
|
|
194848
195439
|
id: genId("trig"),
|
|
@@ -194863,6 +195454,10 @@ function buildTrigger(automationId, input) {
|
|
|
194863
195454
|
hasSigningSecret: false
|
|
194864
195455
|
};
|
|
194865
195456
|
}
|
|
195457
|
+
if (input.kind === "event" && input.event) {
|
|
195458
|
+
t.enabled = false;
|
|
195459
|
+
t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
|
|
195460
|
+
}
|
|
194866
195461
|
return t;
|
|
194867
195462
|
}
|
|
194868
195463
|
function genWebhookToken2() {
|
|
@@ -195166,7 +195761,8 @@ var init_postgres_automations = __esm({
|
|
|
195166
195761
|
t.schedule?.claimedAt ?? null,
|
|
195167
195762
|
t.webhook?.token ?? null,
|
|
195168
195763
|
t.webhook?.provider ?? null,
|
|
195169
|
-
|
|
195764
|
+
// event_filters 一列两用(ADR 0163 §3.2:不新增列):webhook 存过滤数组,event 存订阅配置对象。
|
|
195765
|
+
t.kind === "event" ? JSON.stringify(storedEventConfig(t)) : t.webhook?.eventFilters != null ? JSON.stringify(t.webhook.eventFilters) : null
|
|
195170
195766
|
]
|
|
195171
195767
|
);
|
|
195172
195768
|
}
|
|
@@ -195212,6 +195808,10 @@ var init_postgres_automations = __esm({
|
|
|
195212
195808
|
if (patch.webhook && "eventFilters" in patch.webhook) {
|
|
195213
195809
|
push("event_filters", patch.webhook.eventFilters != null ? JSON.stringify(patch.webhook.eventFilters) : null, "::jsonb");
|
|
195214
195810
|
}
|
|
195811
|
+
if (patch.event?.events) {
|
|
195812
|
+
args.push(JSON.stringify(patch.event.events));
|
|
195813
|
+
sets.push(`event_filters=jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{events}', $${args.length}::jsonb, true)`);
|
|
195814
|
+
}
|
|
195215
195815
|
if (sets.length === 0) return await this.getTrigger(triggerId);
|
|
195216
195816
|
args.push(triggerId);
|
|
195217
195817
|
const r = await this.pool.query(
|
|
@@ -195498,6 +196098,42 @@ var init_postgres_automations = __esm({
|
|
|
195498
196098
|
);
|
|
195499
196099
|
return r.rows.map(rowToTrigger);
|
|
195500
196100
|
}
|
|
196101
|
+
/* ---------- event 触发器(ADR 0163) ---------- */
|
|
196102
|
+
async listEventTriggers() {
|
|
196103
|
+
const r = await this.pool.query(
|
|
196104
|
+
`SELECT t.* FROM ${this.s}.automation_triggers t
|
|
196105
|
+
JOIN ${this.s}.automations a ON a.id = t.automation_id
|
|
196106
|
+
WHERE t.kind = 'event' AND a.enabled AND a.archived_at IS NULL
|
|
196107
|
+
ORDER BY t.id`
|
|
196108
|
+
);
|
|
196109
|
+
return r.rows.map(rowToTrigger);
|
|
196110
|
+
}
|
|
196111
|
+
async completeEventBaseline(triggerId, at) {
|
|
196112
|
+
await this.pool.query(
|
|
196113
|
+
`UPDATE ${this.s}.automation_triggers
|
|
196114
|
+
SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', to_jsonb($2::text), true),
|
|
196115
|
+
enabled = true
|
|
196116
|
+
WHERE id = $1 AND kind = 'event'`,
|
|
196117
|
+
[triggerId, at]
|
|
196118
|
+
);
|
|
196119
|
+
}
|
|
196120
|
+
async resetEventBaseline(triggerId) {
|
|
196121
|
+
await this.pool.query(
|
|
196122
|
+
`UPDATE ${this.s}.automation_triggers
|
|
196123
|
+
SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', 'null'::jsonb, true),
|
|
196124
|
+
enabled = false
|
|
196125
|
+
WHERE id = $1 AND kind = 'event'`,
|
|
196126
|
+
[triggerId]
|
|
196127
|
+
);
|
|
196128
|
+
}
|
|
196129
|
+
async findRunBySpawnedWorkspace(workspace) {
|
|
196130
|
+
const r = await this.pool.query(
|
|
196131
|
+
`SELECT * FROM ${this.s}.automation_runs WHERE spawned_workspace_id = $1
|
|
196132
|
+
ORDER BY triggered_at ASC, id ASC LIMIT 1`,
|
|
196133
|
+
[workspace]
|
|
196134
|
+
);
|
|
196135
|
+
return r.rows[0] ? rowToRun2(r.rows[0]) : null;
|
|
196136
|
+
}
|
|
195501
196137
|
};
|
|
195502
196138
|
rowToAutomation = (row) => ({
|
|
195503
196139
|
id: row.id,
|
|
@@ -195546,6 +196182,15 @@ var init_postgres_automations = __esm({
|
|
|
195546
196182
|
hasSigningSecret: row.signing_secret != null
|
|
195547
196183
|
};
|
|
195548
196184
|
}
|
|
196185
|
+
if (t.kind === "event") {
|
|
196186
|
+
const raw = row.event_filters ?? null;
|
|
196187
|
+
t.event = {
|
|
196188
|
+
source: "project",
|
|
196189
|
+
projectId: raw?.projectId ?? "",
|
|
196190
|
+
events: raw?.events ?? [],
|
|
196191
|
+
baselineState: raw?.baselineAt ? "ready" : "aligning"
|
|
196192
|
+
};
|
|
196193
|
+
}
|
|
195549
196194
|
return t;
|
|
195550
196195
|
};
|
|
195551
196196
|
rowToRun2 = (row) => ({
|
|
@@ -195909,11 +196554,11 @@ var init_postgres_chat_sessions = __esm({
|
|
|
195909
196554
|
});
|
|
195910
196555
|
|
|
195911
196556
|
// ../storage/src/postgres-nodes.ts
|
|
195912
|
-
var
|
|
196557
|
+
var import_node_crypto44, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
|
|
195913
196558
|
var init_postgres_nodes = __esm({
|
|
195914
196559
|
"../storage/src/postgres-nodes.ts"() {
|
|
195915
196560
|
"use strict";
|
|
195916
|
-
|
|
196561
|
+
import_node_crypto44 = require("node:crypto");
|
|
195917
196562
|
init_esm();
|
|
195918
196563
|
ident12 = (s2) => {
|
|
195919
196564
|
if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
|
|
@@ -196062,7 +196707,7 @@ var init_postgres_nodes = __esm({
|
|
|
196062
196707
|
return store;
|
|
196063
196708
|
}
|
|
196064
196709
|
issue(nodeId) {
|
|
196065
|
-
const token = `ont_${(0,
|
|
196710
|
+
const token = `ont_${(0, import_node_crypto44.randomBytes)(24).toString("base64url")}`;
|
|
196066
196711
|
this.cache.set(token, nodeId);
|
|
196067
196712
|
void this.pool.query(`INSERT INTO ${this.s}.node_tokens (token,node_id) VALUES ($1,$2)`, [token, nodeId]);
|
|
196068
196713
|
return token;
|
|
@@ -196274,11 +196919,11 @@ var init_postgres_type_registry = __esm({
|
|
|
196274
196919
|
});
|
|
196275
196920
|
|
|
196276
196921
|
// ../storage/src/postgres-actor-memory.ts
|
|
196277
|
-
var
|
|
196922
|
+
var import_node_crypto45, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
|
|
196278
196923
|
var init_postgres_actor_memory = __esm({
|
|
196279
196924
|
"../storage/src/postgres-actor-memory.ts"() {
|
|
196280
196925
|
"use strict";
|
|
196281
|
-
|
|
196926
|
+
import_node_crypto45 = require("node:crypto");
|
|
196282
196927
|
init_src2();
|
|
196283
196928
|
matchClause = (q) => q.requireMatch && q.keywords.length > 0 ? " WHERE m > 0" : "";
|
|
196284
196929
|
ident15 = (s2) => {
|
|
@@ -196483,7 +197128,7 @@ var init_postgres_actor_memory = __esm({
|
|
|
196483
197128
|
}
|
|
196484
197129
|
async write(input, now) {
|
|
196485
197130
|
if (input.memId === void 0) {
|
|
196486
|
-
const memId = `mem:${(0,
|
|
197131
|
+
const memId = `mem:${(0, import_node_crypto45.randomUUID)()}`;
|
|
196487
197132
|
const r2 = await this.pool.query(
|
|
196488
197133
|
`INSERT INTO ${this.s}.actor_memories
|
|
196489
197134
|
(mem_id, actor_id, project_id, keywords, content, version, created_at, updated_at, accessed_at, source_artifact_id, source_session_id)
|
|
@@ -196996,19 +197641,20 @@ var fs38 = __toESM(require("node:fs"));
|
|
|
196996
197641
|
var os13 = __toESM(require("node:os"));
|
|
196997
197642
|
var path32 = __toESM(require("node:path"));
|
|
196998
197643
|
var import_node_child_process19 = require("node:child_process");
|
|
197644
|
+
var import_node_crypto52 = require("node:crypto");
|
|
196999
197645
|
|
|
197000
197646
|
// ../cli/src/cli.ts
|
|
197001
197647
|
var fs34 = __toESM(require("node:fs"), 1);
|
|
197002
197648
|
var os11 = __toESM(require("node:os"), 1);
|
|
197003
197649
|
var path28 = __toESM(require("node:path"), 1);
|
|
197004
|
-
var
|
|
197650
|
+
var import_node_crypto50 = require("node:crypto");
|
|
197005
197651
|
|
|
197006
197652
|
// ../cli/src/serve.ts
|
|
197007
197653
|
var fs30 = __toESM(require("node:fs"), 1);
|
|
197008
197654
|
var os7 = __toESM(require("node:os"), 1);
|
|
197009
197655
|
var path24 = __toESM(require("node:path"), 1);
|
|
197010
197656
|
var import_node_child_process15 = require("node:child_process");
|
|
197011
|
-
var
|
|
197657
|
+
var import_node_crypto46 = require("node:crypto");
|
|
197012
197658
|
var import_node_url7 = require("node:url");
|
|
197013
197659
|
init_src5();
|
|
197014
197660
|
init_src10();
|
|
@@ -197755,7 +198401,10 @@ async function startServe(opts) {
|
|
|
197755
198401
|
schemaDefs: schema,
|
|
197756
198402
|
registry: kernelRegistry,
|
|
197757
198403
|
reworkWavefront,
|
|
197758
|
-
convergenceUnified
|
|
198404
|
+
convergenceUnified,
|
|
198405
|
+
// ★ ADR 0147:引擎 scan 的 work/review 超时与重试上限走策略中心(与 dispatcher 墙钟、
|
|
198406
|
+
// waiting-inbox 催办同源)。thunk 而非实例——随 SIGHUP / POST /api/policy/reload 热载而变。
|
|
198407
|
+
policy: () => activeTimeoutPolicy
|
|
197759
198408
|
});
|
|
197760
198409
|
const kernel = newEngine.kernel;
|
|
197761
198410
|
const dispatchLedger = await PostgresDispatchStore.open(pgPool, pgSchema);
|
|
@@ -198131,7 +198780,7 @@ async function startServe(opts) {
|
|
|
198131
198780
|
// dispatchChat 收流、done 后落助手消息并回报最终回复。会话在控制台 chat 页可回看。
|
|
198132
198781
|
runChat: async ({ agentId, humanActorId, title, prompt }, onDone) => {
|
|
198133
198782
|
if (!automationDispatchChat) throw new Error("chat \u6D3E\u53D1\u5C1A\u672A\u5C31\u7EEA\uFF08serve \u542F\u52A8\u4E2D\uFF09");
|
|
198134
|
-
const chatSessionId = (0,
|
|
198783
|
+
const chatSessionId = (0, import_node_crypto46.randomUUID)();
|
|
198135
198784
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
198136
198785
|
await chatSessionStore.createSession({
|
|
198137
198786
|
id: chatSessionId,
|
|
@@ -198143,7 +198792,7 @@ async function startServe(opts) {
|
|
|
198143
198792
|
touchedAt: nowIso,
|
|
198144
198793
|
createdAt: nowIso
|
|
198145
198794
|
}).catch(() => void 0);
|
|
198146
|
-
await chatSessionStore.appendMessage({ id: (0,
|
|
198795
|
+
await chatSessionStore.appendMessage({ id: (0, import_node_crypto46.randomUUID)(), sessionId: chatSessionId, role: "user", content: prompt, createdAt: nowIso }).catch(() => void 0);
|
|
198147
198796
|
const session = await automationDispatchChat({ actorId: agentId, message: prompt, chatSessionId });
|
|
198148
198797
|
let buf = "";
|
|
198149
198798
|
session.onOutput((c) => {
|
|
@@ -198152,7 +198801,7 @@ async function startServe(opts) {
|
|
|
198152
198801
|
void session.done.then(async () => {
|
|
198153
198802
|
if (buf || session.runId) {
|
|
198154
198803
|
await chatSessionStore.appendMessage({
|
|
198155
|
-
id: (0,
|
|
198804
|
+
id: (0, import_node_crypto46.randomUUID)(),
|
|
198156
198805
|
sessionId: chatSessionId,
|
|
198157
198806
|
role: "assistant",
|
|
198158
198807
|
content: buf,
|
|
@@ -198166,6 +198815,10 @@ async function startServe(opts) {
|
|
|
198166
198815
|
});
|
|
198167
198816
|
return { chatSessionId };
|
|
198168
198817
|
},
|
|
198818
|
+
// 订阅触发 × chat 的 fail-closed 开关(ADR 0163 §4.4)。当前 runChat 每次 randomUUID 建会话、
|
|
198819
|
+
// 没有任何持久去重:占位后崩溃重试会多开一个会话。**在 chat 适配器补上持久幂等键之前保持 false**,
|
|
198820
|
+
// 由此 event+chat 在配置期就被拒(能力位 supported=false),而不是上线后偶发重复开会话。
|
|
198821
|
+
chatSupportsIdempotency: false,
|
|
198169
198822
|
// chat 准入(≈ multica AgentReadiness):在岗 + 有 active runtime 绑定,注定失败的触发别去开会话。
|
|
198170
198823
|
agentReady: async (agentId) => {
|
|
198171
198824
|
const actor = await registryStore.getActor(agentId);
|
|
@@ -198183,6 +198836,13 @@ async function startServe(opts) {
|
|
|
198183
198836
|
const member = await controlPlaneStore.getMember(companyId, actor);
|
|
198184
198837
|
return member?.role === "owner" || member?.role === "admin";
|
|
198185
198838
|
},
|
|
198839
|
+
// 订阅项目可读判据(ADR 0163 §5.2):复用 projects 域的可见性口径,automation 侧不复制成员规则。
|
|
198840
|
+
// 现状:projects 域对登录成员不做逐项目读限制,故「可读」= 项目存在;项目被删/查不到 → null → 404。
|
|
198841
|
+
// 哪天 projects 加了成员级可见性,改这一个闭包即可,automation 侧不动。
|
|
198842
|
+
projectAccess: async ({ projectId }) => {
|
|
198843
|
+
const project = await projectStateStore.getProject(projectId);
|
|
198844
|
+
return project ? { id: project.id, name: project.name } : null;
|
|
198845
|
+
},
|
|
198186
198846
|
log: (m2) => console.log(m2)
|
|
198187
198847
|
});
|
|
198188
198848
|
const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, pgSchema) : await FileControlPlaneStore.open(path24.join(opts.dir, "control-plane.json"));
|
|
@@ -198353,7 +199013,9 @@ async function startServe(opts) {
|
|
|
198353
199013
|
schemaDefs: schema,
|
|
198354
199014
|
registry: kernelRegistry,
|
|
198355
199015
|
reworkWavefront,
|
|
198356
|
-
convergenceUnified
|
|
199016
|
+
convergenceUnified,
|
|
199017
|
+
// ★ 同默认公司:漏了这里,非默认公司的引擎照旧按代码默认 30min 判超时(策略文件形同虚设)。
|
|
199018
|
+
policy: () => activeTimeoutPolicy
|
|
198357
199019
|
});
|
|
198358
199020
|
await ensureCompanyActors(controlPlaneStore, companyId, registryStore);
|
|
198359
199021
|
await syncRegistryRolesToKernel(registryStore, engine2.kernel);
|
|
@@ -198604,7 +199266,7 @@ async function startServe(opts) {
|
|
|
198604
199266
|
}));
|
|
198605
199267
|
}
|
|
198606
199268
|
const limits = { wallClockMs: wallClockForKind("planner") };
|
|
198607
|
-
const artifactId = `artifact:planner:${(0,
|
|
199269
|
+
const artifactId = `artifact:planner:${(0, import_node_crypto46.randomUUID)()}`;
|
|
198608
199270
|
const handle = await chatRemoteAdapter.spawn({
|
|
198609
199271
|
actor: planner.id,
|
|
198610
199272
|
// companyId 同协调者:planner 用的就是默认公司的 actors 服务,漏签会让它一调公司域端点就 404。
|
|
@@ -199021,10 +199683,10 @@ async function startServe(opts) {
|
|
|
199021
199683
|
{ nodeId: priorChatSession?.runtimeId, runtimeKind: priorChatSession?.runtimeKind },
|
|
199022
199684
|
{ nodeId: binding.nodeId, runtimeKind: binding.runtimeKind }
|
|
199023
199685
|
) : false;
|
|
199024
|
-
const runtimeSessionId = sessionId ?? (0,
|
|
199686
|
+
const runtimeSessionId = sessionId ?? (0, import_node_crypto46.randomUUID)();
|
|
199025
199687
|
const resumeRuntimeSession = Boolean(sessionId);
|
|
199026
|
-
const traceRunId = `chat-run:${(0,
|
|
199027
|
-
const artifactId = `artifact:chat:${(0,
|
|
199688
|
+
const traceRunId = `chat-run:${(0, import_node_crypto46.randomUUID)()}`;
|
|
199689
|
+
const artifactId = `artifact:chat:${(0, import_node_crypto46.randomUUID)()}`;
|
|
199028
199690
|
const traceStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
199029
199691
|
let lastProgressTouchMs = 0;
|
|
199030
199692
|
const PROGRESS_TOUCH_THROTTLE_MS2 = 2e4;
|
|
@@ -199677,7 +200339,7 @@ async function startServe(opts) {
|
|
|
199677
200339
|
console.log(`[new-engine] dispatchWork ${workId} \u662F human work\u2014\u2014\u4E0D\u6D3E runtime\uFF0C\u7B49\u5F85\u4EBA\u5DE5\u5904\u7406`);
|
|
199678
200340
|
return;
|
|
199679
200341
|
}
|
|
199680
|
-
const ledgerId = `dispatch:${(0,
|
|
200342
|
+
const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
|
|
199681
200343
|
try {
|
|
199682
200344
|
await dispatchLedger.insertOpen({
|
|
199683
200345
|
id: ledgerId,
|
|
@@ -199753,7 +200415,7 @@ async function startServe(opts) {
|
|
|
199753
200415
|
const kernelModel = newKernel.model;
|
|
199754
200416
|
const art = kernelModel.artifacts.get(nodeId);
|
|
199755
200417
|
const revWid = art?.workspace ?? "";
|
|
199756
|
-
const ledgerId = `dispatch:${(0,
|
|
200418
|
+
const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
|
|
199757
200419
|
try {
|
|
199758
200420
|
await dispatchLedger.insertOpen({
|
|
199759
200421
|
id: ledgerId,
|
|
@@ -200875,6 +201537,31 @@ ${st.detail}
|
|
|
200875
201537
|
const automationTimer = setInterval(() => {
|
|
200876
201538
|
void automationScheduler.tick().catch((err) => console.error(`[automation-scheduler] tick \u5931\u8D25: ${String(err)}`));
|
|
200877
201539
|
}, 3e4);
|
|
201540
|
+
const eventPollMs = Number(process.env["OASIS_AUTOMATION_EVENT_POLL_MS"] ?? 1e4);
|
|
201541
|
+
let automationEventTimer;
|
|
201542
|
+
if (Number.isFinite(eventPollMs) && eventPollMs > 0) {
|
|
201543
|
+
const eventPoller = new ProjectEventPoller({
|
|
201544
|
+
store: automationStore,
|
|
201545
|
+
service: automations.service,
|
|
201546
|
+
log: (m2) => console.log(m2),
|
|
201547
|
+
batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
|
|
201548
|
+
// 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
|
|
201549
|
+
listProjectWorkspaces: async ({ projectId }) => (await artifactStateStore.listWorkspaceBindings()).filter((b2) => b2.projectId === projectId).map((b2) => b2.workOrderId),
|
|
201550
|
+
// 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
|
|
201551
|
+
workorderSnapshot: async ({ companyId, workspace }) => {
|
|
201552
|
+
const engine2 = await getEngine(companyId);
|
|
201553
|
+
return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
|
|
201554
|
+
},
|
|
201555
|
+
// 触发时复查归因主体的项目读取权(与配置期同一口径)。
|
|
201556
|
+
projectReadable: async ({ projectId }) => await projectStateStore.getProject(projectId) !== null
|
|
201557
|
+
});
|
|
201558
|
+
automationEventTimer = setInterval(() => {
|
|
201559
|
+
void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
|
|
201560
|
+
}, eventPollMs);
|
|
201561
|
+
console.log(`[serve] \u9879\u76EE\u4E8B\u4EF6\u8BA2\u9605\u8F6E\u8BE2\u5668\u5DF2\u542F\u7528\uFF08\u6BCF ${Math.round(eventPollMs / 1e3)}s \u4E00\u8F6E\uFF1BOASIS_AUTOMATION_EVENT_POLL_MS=0 \u53EF\u5173\uFF09`);
|
|
201562
|
+
} else {
|
|
201563
|
+
console.log("[serve] \u9879\u76EE\u4E8B\u4EF6\u8BA2\u9605\u8F6E\u8BE2\u5668\u5DF2\u88AB OASIS_AUTOMATION_EVENT_POLL_MS \u663E\u5F0F\u5173\u95ED");
|
|
201564
|
+
}
|
|
200878
201565
|
let boardWatchTimer;
|
|
200879
201566
|
const boardWatchFlag = (process.env["OASIS_BOARD_WATCH"] ?? "").trim().toLowerCase();
|
|
200880
201567
|
if (!["0", "false", "off"].includes(boardWatchFlag)) {
|
|
@@ -200900,6 +201587,7 @@ ${st.detail}
|
|
|
200900
201587
|
if (orphanSweepTimer) clearInterval(orphanSweepTimer);
|
|
200901
201588
|
if (zeroOutputBreakerTimer) clearInterval(zeroOutputBreakerTimer);
|
|
200902
201589
|
clearInterval(automationTimer);
|
|
201590
|
+
if (automationEventTimer) clearInterval(automationEventTimer);
|
|
200903
201591
|
if (boardWatchTimer) clearInterval(boardWatchTimer);
|
|
200904
201592
|
clearInterval(serverHeartbeatTimer);
|
|
200905
201593
|
hub?.close();
|
|
@@ -200967,7 +201655,7 @@ var SessionManager = class {
|
|
|
200967
201655
|
// ../cli/src/daemon/ws-client.ts
|
|
200968
201656
|
init_wrapper();
|
|
200969
201657
|
var import_node_os9 = require("node:os");
|
|
200970
|
-
var
|
|
201658
|
+
var import_node_crypto47 = require("node:crypto");
|
|
200971
201659
|
init_src8();
|
|
200972
201660
|
init_src7();
|
|
200973
201661
|
|
|
@@ -201279,6 +201967,9 @@ var PENDING_STREAM_MAX = 2e3;
|
|
|
201279
201967
|
var AUTH_REJECT_BACKOFF_MAX_MS = 15 * 6e4;
|
|
201280
201968
|
var AUTH_REJECT_GIVE_UP_AFTER = 3;
|
|
201281
201969
|
var AUTH_REJECT_RE = /Unexpected server response:\s*(401|403)/;
|
|
201970
|
+
var DRAIN_TIMEOUT_MS = 15e3;
|
|
201971
|
+
var DRAIN_POLL_MS = 50;
|
|
201972
|
+
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
201282
201973
|
async function preflightOasisAccess(job) {
|
|
201283
201974
|
const base = job.server.url.replace(/\/+$/, "");
|
|
201284
201975
|
try {
|
|
@@ -201323,6 +202014,8 @@ var DaemonWsClient = class {
|
|
|
201323
202014
|
stopped = false;
|
|
201324
202015
|
/** 收到 update 但有活跃会话时置真:延后到所有会话结束再自更新,避免打断执行中的 agent。 */
|
|
201325
202016
|
pendingUpdate = false;
|
|
202017
|
+
/** 停机排空中(见 {@link drain}):不再接新派发、不再触发延后的自更新。 */
|
|
202018
|
+
draining = false;
|
|
201326
202019
|
/** 已回 dispatch_received、尚未完成 runtime spawn 的派发;也随 hello 上报供重连对账。 */
|
|
201327
202020
|
pendingDispatchIds = /* @__PURE__ */ new Set();
|
|
201328
202021
|
/** setup 期间收到 kill 的派发。spawn 返回后先杀句柄,不得再宣告 session_started。 */
|
|
@@ -201379,6 +202072,73 @@ var DaemonWsClient = class {
|
|
|
201379
202072
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
201380
202073
|
this.ws?.close();
|
|
201381
202074
|
}
|
|
202075
|
+
/** 排空中?(观测/测试用) */
|
|
202076
|
+
isDraining() {
|
|
202077
|
+
return this.draining;
|
|
202078
|
+
}
|
|
202079
|
+
/**
|
|
202080
|
+
* 停机排空——在进程退出前,把「在途会话已经死了」这件事告诉服务端。
|
|
202081
|
+
*
|
|
202082
|
+
* ## 为什么必须有这一步(2026-08-25 生产实证)
|
|
202083
|
+
*
|
|
202084
|
+
* 引擎里一条 work 从 `running` 变成别的状态,全后端只有三个入口:收到 `session_exited` 帧走主路
|
|
202085
|
+
* (`serve.ts` 的 work.response 桥)、同一帧走 journal 反查的兜底、以及 scan 里 **30 分钟**的
|
|
202086
|
+
* `work.timeout`。前两个都要求**这一帧真的被发出来**。
|
|
202087
|
+
*
|
|
202088
|
+
* 而这一帧的生成者,是本进程 dispatch 分支里注册的那个 `handle.onExit` 回调——**它住在 daemon
|
|
202089
|
+
* 进程内部**。子进程被杀不会自动产生任何帧;进程一死,回调就再没机会跑。
|
|
202090
|
+
*
|
|
202091
|
+
* 旧行为:SIGTERM handler 只删 pid 文件就 `process.exit(0)`;systemd unit 又没设 `KillMode`,
|
|
202092
|
+
* 默认 `control-group` 把 daemon 和它拉起的所有会话**同时** SIGTERM——父子同死,一帧都发不出去。
|
|
202093
|
+
* 实测 node-6745008c1fc7:5389 次会话起跑里 48 个没有配对退出帧,其中 43 个(90%)是重启带走的;
|
|
202094
|
+
* 每一个都让对应的 work 空转到 30 分钟超时才被判死,控制台全程显示"运行中"。
|
|
202095
|
+
*
|
|
202096
|
+
* ## 做法:让真实退出帧走正路,只有两类才合成
|
|
202097
|
+
*
|
|
202098
|
+
* 不图省事直接合成假帧——`kill()` 掉在途会话,adapter 的 `killReason` 归因成 `cancelled`,既有
|
|
202099
|
+
* `onExit` 正路会带着 `transcriptRef` 把**真实**帧发出去。只有两类兜底才合成:还没 spawn 的派发
|
|
202100
|
+
* (没有子进程可 kill,不可能有真实帧),以及超时后仍未退出的会话(宁可给服务端一帧不精确的死讯,
|
|
202101
|
+
* 也好过让它等满 30 分钟)。
|
|
202102
|
+
*
|
|
202103
|
+
* 排空期间 `draining` 置真:不再接新派发(见 dispatch 分支),也不再触发延后的自更新。
|
|
202104
|
+
*/
|
|
202105
|
+
async drain(reason, timeoutMs = DRAIN_TIMEOUT_MS) {
|
|
202106
|
+
if (this.draining) return;
|
|
202107
|
+
this.draining = true;
|
|
202108
|
+
const active = this.sessions.activeSessions();
|
|
202109
|
+
const pending = [...this.pendingDispatchIds];
|
|
202110
|
+
if (active.length === 0 && pending.length === 0) {
|
|
202111
|
+
this.stop();
|
|
202112
|
+
return;
|
|
202113
|
+
}
|
|
202114
|
+
log2("[node-cli]", `\u505C\u673A\u6392\u7A7A\uFF08${reason}\uFF09\uFF1A\u5728\u9014\u4F1A\u8BDD ${active.length} \u4E2A\u3001\u5F85\u8D77\u6D3E\u53D1 ${pending.length} \u4E2A\u2014\u2014\u53D1\u5B8C\u9000\u51FA\u5E27\u518D\u9000\u51FA`);
|
|
202115
|
+
for (const dispatchId of pending) {
|
|
202116
|
+
this.pendingDispatchIds.delete(dispatchId);
|
|
202117
|
+
this.sendKeyFrame(dispatchId, {
|
|
202118
|
+
type: "session_exited",
|
|
202119
|
+
dispatchId,
|
|
202120
|
+
sessionId: "node-shutdown",
|
|
202121
|
+
info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u6D3E\u53D1\u5C1A\u672A\u8D77\u4F1A\u8BDD` }
|
|
202122
|
+
});
|
|
202123
|
+
}
|
|
202124
|
+
await Promise.all(active.map((s2) => this.sessions.kill(s2.dispatchId).catch(() => void 0)));
|
|
202125
|
+
const deadline = Date.now() + timeoutMs;
|
|
202126
|
+
while (Date.now() < deadline && (this.sessions.activeCount() > 0 || this.outbox.size > 0)) await sleep4(DRAIN_POLL_MS);
|
|
202127
|
+
const stuck = this.sessions.activeSessions();
|
|
202128
|
+
for (const s2 of stuck) {
|
|
202129
|
+
log2("[node-cli]", ` \u26A0 \u4F1A\u8BDD ${s2.sessionId} \u5728 ${timeoutMs}ms \u5185\u6CA1\u9000\u51FA\uFF0C\u6309\u505C\u673A\u5408\u6210\u9000\u51FA\u5E27`);
|
|
202130
|
+
this.sendKeyFrame(s2.dispatchId, {
|
|
202131
|
+
type: "session_exited",
|
|
202132
|
+
dispatchId: s2.dispatchId,
|
|
202133
|
+
sessionId: s2.sessionId,
|
|
202134
|
+
info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u4F1A\u8BDD\u672A\u5728\u6392\u7A7A\u7A97\u53E3\u5185\u9000\u51FA` }
|
|
202135
|
+
});
|
|
202136
|
+
}
|
|
202137
|
+
if (stuck.length > 0) await sleep4(DRAIN_POLL_MS * 4);
|
|
202138
|
+
if (this.outbox.size > 0) log2("[node-cli]", `\u26A0 \u505C\u673A\u6392\u7A7A\u7ED3\u675F\uFF1A\u4ECD\u6709 ${this.outbox.size} \u4E2A\u9000\u51FA\u5E27\u672A\u786E\u8BA4\u9001\u8FBE\uFF08\u8FD9\u4E9B work \u4F1A\u9000\u56DE 30 \u5206\u949F\u8D85\u65F6\u515C\u5E95\uFF09`);
|
|
202139
|
+
else log2("[node-cli]", "\u505C\u673A\u6392\u7A7A\u5B8C\u6210\uFF1A\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u5747\u5DF2\u786E\u8BA4\u9001\u8FBE");
|
|
202140
|
+
this.stop();
|
|
202141
|
+
}
|
|
201382
202142
|
connect() {
|
|
201383
202143
|
if (this.stopped) return;
|
|
201384
202144
|
const ws = new import_websocket.default(this.serverUrl, {
|
|
@@ -201455,6 +202215,16 @@ var DaemonWsClient = class {
|
|
|
201455
202215
|
const { dispatchId, job } = msg;
|
|
201456
202216
|
const t0 = Date.now();
|
|
201457
202217
|
log2("[node-cli]", `\u2190 dispatch ${dispatchId} artifact=${job.artifactId}`);
|
|
202218
|
+
if (this.draining) {
|
|
202219
|
+
log2("[node-cli]", ` \u62D2\u6536\uFF1A\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A \u2192 \u56DE\u9000\u51FA\u5E27\uFF0C\u670D\u52A1\u7AEF\u53EF\u7ACB\u5373\u6539\u6D3E`);
|
|
202220
|
+
this.sendKeyFrame(dispatchId, {
|
|
202221
|
+
type: "session_exited",
|
|
202222
|
+
dispatchId,
|
|
202223
|
+
sessionId: "node-draining",
|
|
202224
|
+
info: { code: null, reason: "cancelled", errorMessage: "\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A\uFF0C\u672C\u6B21\u6D3E\u53D1\u672A\u8D77\u4F1A\u8BDD" }
|
|
202225
|
+
});
|
|
202226
|
+
break;
|
|
202227
|
+
}
|
|
201458
202228
|
if (this.pendingDispatchIds.has(dispatchId) || this.sessions.get(dispatchId)) {
|
|
201459
202229
|
this.send({ type: "dispatch_received", dispatchId });
|
|
201460
202230
|
break;
|
|
@@ -201572,6 +202342,10 @@ var DaemonWsClient = class {
|
|
|
201572
202342
|
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");
|
|
201573
202343
|
break;
|
|
201574
202344
|
}
|
|
202345
|
+
if (this.draining) {
|
|
202346
|
+
log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u6B63\u5728\u505C\u673A\u6392\u7A7A");
|
|
202347
|
+
break;
|
|
202348
|
+
}
|
|
201575
202349
|
const active = this.sessions.activeCount();
|
|
201576
202350
|
if (active > 0) {
|
|
201577
202351
|
this.pendingUpdate = true;
|
|
@@ -201592,7 +202366,7 @@ var DaemonWsClient = class {
|
|
|
201592
202366
|
*/
|
|
201593
202367
|
async queryArtifactStates(artifactIds, timeoutMs = 15e3) {
|
|
201594
202368
|
if (artifactIds.length === 0) return {};
|
|
201595
|
-
const requestId = (0,
|
|
202369
|
+
const requestId = (0, import_node_crypto47.randomUUID)();
|
|
201596
202370
|
return new Promise((resolve10, reject) => {
|
|
201597
202371
|
const timer = setTimeout(() => {
|
|
201598
202372
|
this.gcPending.delete(requestId);
|
|
@@ -201659,6 +202433,7 @@ var DaemonWsClient = class {
|
|
|
201659
202433
|
}
|
|
201660
202434
|
/** 会话结束后调用:若之前收到过 update 但因忙延后了,且现已空闲,则触发自更新。 */
|
|
201661
202435
|
maybeRunPendingUpdate() {
|
|
202436
|
+
if (this.draining) return;
|
|
201662
202437
|
if (this.pendingUpdate && this.sessions.activeCount() === 0) {
|
|
201663
202438
|
this.pendingUpdate = false;
|
|
201664
202439
|
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");
|
|
@@ -201822,6 +202597,7 @@ async function startNode(opts) {
|
|
|
201822
202597
|
});
|
|
201823
202598
|
console.log(`[oasis node] ${opts.nodeId} \u2192 ${opts.serverUrl}`);
|
|
201824
202599
|
client.start();
|
|
202600
|
+
opts.onReady?.({ drain: (reason) => client.drain(reason) });
|
|
201825
202601
|
const { workRoot, legacyRoot } = resolveWorkRoots(opts.workRoot);
|
|
201826
202602
|
const workRootIsEphemeral = workRoot.startsWith(import_node_os11.default.tmpdir());
|
|
201827
202603
|
if (opts.gcEnabled === false && !workRootIsEphemeral) {
|
|
@@ -201929,7 +202705,7 @@ function forwardSignal(child, sig, killGroup = (pgid, signal) => {
|
|
|
201929
202705
|
// ../cli/src/daemon/machine-id.ts
|
|
201930
202706
|
var import_node_child_process17 = require("node:child_process");
|
|
201931
202707
|
var import_node_fs21 = require("node:fs");
|
|
201932
|
-
var
|
|
202708
|
+
var import_node_crypto48 = require("node:crypto");
|
|
201933
202709
|
var import_node_os12 = require("node:os");
|
|
201934
202710
|
function linuxMachineId() {
|
|
201935
202711
|
for (const p2 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
@@ -201995,7 +202771,7 @@ var defaultSources = {
|
|
|
201995
202771
|
function resolveNodeId(sources = {}) {
|
|
201996
202772
|
const s2 = { ...defaultSources, ...sources };
|
|
201997
202773
|
const material = `${s2.machineFingerprint()}:${s2.osUser()}`;
|
|
201998
|
-
const digest = (0,
|
|
202774
|
+
const digest = (0, import_node_crypto48.createHash)("sha256").update(material).digest("hex").slice(0, 12);
|
|
201999
202775
|
return `node-${digest}`;
|
|
202000
202776
|
}
|
|
202001
202777
|
|
|
@@ -202329,17 +203105,22 @@ var COMMAND_DECLS = {
|
|
|
202329
203105
|
},
|
|
202330
203106
|
"automation-trigger-add": {
|
|
202331
203107
|
group: "\u81EA\u52A8\u5316",
|
|
202332
|
-
usage: "oasis automation-trigger-add <id> --kind schedule|webhook [--cron <expr> --tz <tz>] [--provider generic|github] [--label <\u540D>]",
|
|
202333
|
-
description: "\u7ED9\u81EA\u52A8\u5316\u8FFD\u52A0\u4E00\u6761\u89E6\u53D1\u5668\uFF08\u4E00\u6761\u81EA\u52A8\u5316\u53EF\u6302\u591A\u4E2A\uFF09\u3002",
|
|
203108
|
+
usage: "oasis automation-trigger-add <id> --kind schedule|webhook|event [--cron <expr> --tz <tz>] [--provider generic|github] [--project <projectId> --event workorder.completed] [--label <\u540D>]",
|
|
203109
|
+
description: "\u7ED9\u81EA\u52A8\u5316\u8FFD\u52A0\u4E00\u6761\u89E6\u53D1\u5668\uFF08\u4E00\u6761\u81EA\u52A8\u5316\u53EF\u6302\u591A\u4E2A\uFF09\u3002event=\u8BA2\u9605\u9879\u76EE\uFF1A\u8BE5\u9879\u76EE\u4E0B\u5DE5\u5355\u5B8C\u6210\u65F6\u89E6\u53D1\u4E00\u6B21\uFF1B\u65B0\u5EFA\u540E\u5148\u8DD1\u4E00\u6B21\u57FA\u7EBF\u5BF9\u9F50\uFF08\u4E0D\u56DE\u653E\u5386\u53F2\uFF09\uFF0C\u5BF9\u9F50\u5B8C\u81EA\u52A8\u542F\u7528\u3002",
|
|
202334
203110
|
positional: [{ name: "<id>", required: true, desc: "\u81EA\u52A8\u5316 id" }],
|
|
202335
203111
|
flags: [
|
|
202336
|
-
{ name: "kind", desc: "schedule|webhook\uFF08\u9ED8\u8BA4 schedule\uFF09" },
|
|
203112
|
+
{ name: "kind", desc: "schedule|webhook|event\uFF08\u9ED8\u8BA4 schedule\uFF09" },
|
|
202337
203113
|
{ name: "cron", desc: "5 \u5B57\u6BB5 cron\uFF08schedule \u5FC5\u586B\uFF09" },
|
|
202338
203114
|
{ name: "tz", desc: "IANA \u65F6\u533A\uFF08\u9ED8\u8BA4 UTC\uFF09" },
|
|
202339
203115
|
{ name: "provider", desc: "webhook provider\uFF1Ageneric|github" },
|
|
203116
|
+
{ name: "project", desc: "\u8BA2\u9605\u7684\u9879\u76EE id\uFF08event \u5FC5\u586B\uFF1Bschedule/webhook \u4E0D\u63A5\u53D7\uFF09" },
|
|
203117
|
+
{ name: "event", desc: "\u8BA2\u9605\u7684\u4E8B\u4EF6\u540D\uFF08event \u5FC5\u586B\uFF0C\u53EF\u91CD\u590D\u6216\u9017\u53F7\u5206\u9694\uFF1B\u672C\u671F\u53EA\u6709 workorder.completed\uFF09" },
|
|
202340
203118
|
{ name: "label", desc: "\u663E\u793A\u540D" }
|
|
202341
203119
|
],
|
|
202342
|
-
examples: [
|
|
203120
|
+
examples: [
|
|
203121
|
+
'oasis automation-trigger-add auto_abc123 --kind schedule --cron "0 18 * * 5" --tz Asia/Shanghai',
|
|
203122
|
+
"oasis automation-trigger-add auto_abc123 --kind event --project oasis --event workorder.completed"
|
|
203123
|
+
]
|
|
202343
203124
|
},
|
|
202344
203125
|
"automation-trigger-update": {
|
|
202345
203126
|
group: "\u81EA\u52A8\u5316",
|
|
@@ -203607,13 +204388,13 @@ function ownFlagsFromDecl(command) {
|
|
|
203607
204388
|
}
|
|
203608
204389
|
|
|
203609
204390
|
// ../cli/src/connector-effect-runner.ts
|
|
203610
|
-
var
|
|
204391
|
+
var import_node_crypto49 = require("node:crypto");
|
|
203611
204392
|
var import_node_child_process18 = require("node:child_process");
|
|
203612
204393
|
init_src8();
|
|
203613
204394
|
function stableKey(effect) {
|
|
203614
204395
|
const keyFields = Object.fromEntries(Object.entries(effect.keyFields).sort(([a], [b2]) => a.localeCompare(b2)));
|
|
203615
204396
|
const canonical = JSON.stringify({ kind: effect.kind, target: effect.target, keyFields });
|
|
203616
|
-
return `sha256:${(0,
|
|
204397
|
+
return `sha256:${(0, import_node_crypto49.createHash)("sha256").update(canonical).digest("hex")}`;
|
|
203617
204398
|
}
|
|
203618
204399
|
async function runConnectorEffect(connector, toolArgs, deps) {
|
|
203619
204400
|
const effect = connector.describeExternalEffect?.(toolArgs) ?? null;
|
|
@@ -204208,7 +204989,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
|
|
|
204208
204989
|
const store = createNodeTokenStore(path28.join(dir, "node-tokens.json"));
|
|
204209
204990
|
const sub = positional[0];
|
|
204210
204991
|
if (sub === "issue") {
|
|
204211
|
-
const id = flags.get("id") ?? `node-${(0,
|
|
204992
|
+
const id = flags.get("id") ?? `node-${(0, import_node_crypto50.randomUUID)()}`;
|
|
204212
204993
|
const token2 = store.issue(id);
|
|
204213
204994
|
println(token2);
|
|
204214
204995
|
process.stderr.write(
|
|
@@ -204821,10 +205602,36 @@ ${p2.ref} ${p2.name}`);
|
|
|
204821
205602
|
break;
|
|
204822
205603
|
}
|
|
204823
205604
|
case "automation-trigger-add": {
|
|
204824
|
-
const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook ...");
|
|
205605
|
+
const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook|event ...");
|
|
204825
205606
|
const kind = flags.get("kind") ?? "schedule";
|
|
204826
|
-
|
|
204827
|
-
|
|
205607
|
+
if (kind !== "schedule" && kind !== "webhook" && kind !== "event") {
|
|
205608
|
+
throw new Error(`--kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 event\uFF08\u6536\u5230 ${kind}\uFF09`);
|
|
205609
|
+
}
|
|
205610
|
+
if (kind !== "event" && (flags.has("project") || flags.has("event"))) {
|
|
205611
|
+
throw new Error(`--project / --event \u53EA\u7528\u4E8E --kind event\uFF08\u5F53\u524D --kind ${kind}\uFF09`);
|
|
205612
|
+
}
|
|
205613
|
+
if (kind === "event" && (flags.has("cron") || flags.has("tz") || flags.has("provider"))) {
|
|
205614
|
+
throw new Error("--kind event \u4E0D\u63A5\u53D7 --cron / --tz / --provider");
|
|
205615
|
+
}
|
|
205616
|
+
const label = flags.has("label") ? { label: flags.get("label") } : {};
|
|
205617
|
+
let body;
|
|
205618
|
+
if (kind === "schedule") {
|
|
205619
|
+
body = { kind, schedule: { cron: need(flags, "cron"), tz: flags.get("tz") ?? "UTC" }, ...label };
|
|
205620
|
+
} else if (kind === "webhook") {
|
|
205621
|
+
body = { kind, webhook: { provider: flags.get("provider") ?? "generic" }, ...label };
|
|
205622
|
+
} else {
|
|
205623
|
+
const events = (flags.get("event") ?? "").split(",").map((x2) => x2.trim()).filter(Boolean);
|
|
205624
|
+
if (!events.length) throw new Error("--kind event \u9700\u8981\u81F3\u5C11\u4E00\u4E2A --event\uFF08\u5982 --event workorder.completed\uFF09");
|
|
205625
|
+
body = { kind, event: { source: "project", projectId: need(flags, "project"), events }, ...label };
|
|
205626
|
+
}
|
|
205627
|
+
const created = await api.post(
|
|
205628
|
+
`/api/automations/${encodeURIComponent(id)}/triggers`,
|
|
205629
|
+
body
|
|
205630
|
+
);
|
|
205631
|
+
println(JSON.stringify(created, null, 2));
|
|
205632
|
+
if (kind === "event" && created.event?.baselineState === "aligning") {
|
|
205633
|
+
println("\u2139 \u8BA2\u9605\u5DF2\u5EFA\uFF0C\u6B63\u5728\u5BF9\u9F50\u57FA\u7EBF\uFF1A\u628A\u9879\u76EE\u91CC**\u5DF2\u5B8C\u6210**\u7684\u5DE5\u5355\u8BB0\u4E3A\u300C\u5DF2\u77E5\u300D\uFF0C\u5BF9\u9F50\u5B8C\u81EA\u52A8\u542F\u7528\u3002\u5386\u53F2\u5DE5\u5355\u4E0D\u4F1A\u88AB\u8865\u89E6\u53D1\u3002");
|
|
205634
|
+
}
|
|
204828
205635
|
break;
|
|
204829
205636
|
}
|
|
204830
205637
|
case "automation-trigger-update": {
|
|
@@ -206673,12 +207480,12 @@ var LEGACY_SYSTEMD_UNIT = "oasis-node.service";
|
|
|
206673
207480
|
var LEGACY_LAUNCHD_LABEL = "com.oasis.node";
|
|
206674
207481
|
|
|
206675
207482
|
// src/install.ts
|
|
206676
|
-
var
|
|
207483
|
+
var import_node_crypto51 = require("node:crypto");
|
|
206677
207484
|
var fs37 = __toESM(require("node:fs"));
|
|
206678
207485
|
var path31 = __toESM(require("node:path"));
|
|
206679
207486
|
function versionLabel(sourceFile, pkgVersion, npmPrefixDir) {
|
|
206680
207487
|
if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return pkgVersion;
|
|
206681
|
-
const digest = (0,
|
|
207488
|
+
const digest = (0, import_node_crypto51.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
|
|
206682
207489
|
return `${pkgVersion}+local.${digest}`;
|
|
206683
207490
|
}
|
|
206684
207491
|
function isFromNpmPrefix(sourceFile, npmPrefixDir) {
|
|
@@ -206758,6 +207565,37 @@ function gcVersions(versionsDirPath, opts) {
|
|
|
206758
207565
|
}
|
|
206759
207566
|
return deleted;
|
|
206760
207567
|
}
|
|
207568
|
+
function systemdUnitText(o) {
|
|
207569
|
+
return [
|
|
207570
|
+
"[Unit]",
|
|
207571
|
+
`Description=Oasis Node Daemon (${o.name})`,
|
|
207572
|
+
"After=network.target",
|
|
207573
|
+
"",
|
|
207574
|
+
// OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
|
|
207575
|
+
// 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
|
|
207576
|
+
// (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
|
|
207577
|
+
"[Service]",
|
|
207578
|
+
`ExecStart=${o.cmd}`,
|
|
207579
|
+
`Environment=PATH=${o.daemonPath}`,
|
|
207580
|
+
`Environment=OASIS_CLI_HOME=${o.cliHome}`,
|
|
207581
|
+
`StandardOutput=append:${o.logFile}`,
|
|
207582
|
+
`StandardError=append:${o.logFile}`,
|
|
207583
|
+
"Restart=on-failure",
|
|
207584
|
+
"RestartSec=5",
|
|
207585
|
+
// 🚨 停机时只 SIGTERM 主进程,子会话留给 daemon 自己收——它要先给每个在途会话发 session_exited,
|
|
207586
|
+
// 那是服务端唯一的死亡通知渠道(生成者是 daemon 进程内的 onExit 回调)。
|
|
207587
|
+
// systemd 默认的 KillMode=control-group 会把 daemon 和它拉起的所有 claude 会话**同时**杀掉:
|
|
207588
|
+
// 父子同死,一帧都发不出去,对应的 work 只能空转到 scan 的 30 分钟超时才被判死。
|
|
207589
|
+
// 实测占本机孤儿会话的 90%(43/48)。详见 change-record 20260825-daemon-restart-swallows-exit-frames。
|
|
207590
|
+
// TimeoutStopSec 必须明显大于排空预算 DRAIN_TIMEOUT_MS(15s),否则排空没做完就被 SIGKILL。
|
|
207591
|
+
"KillMode=mixed",
|
|
207592
|
+
"TimeoutStopSec=30",
|
|
207593
|
+
"",
|
|
207594
|
+
"[Install]",
|
|
207595
|
+
"WantedBy=default.target",
|
|
207596
|
+
""
|
|
207597
|
+
].join("\n");
|
|
207598
|
+
}
|
|
206761
207599
|
function shimScript() {
|
|
206762
207600
|
return [
|
|
206763
207601
|
"#!/bin/sh",
|
|
@@ -206770,7 +207608,7 @@ function shimScript() {
|
|
|
206770
207608
|
}
|
|
206771
207609
|
|
|
206772
207610
|
// src/index.ts
|
|
206773
|
-
var PKG_VERSION = true ? "0.1.
|
|
207611
|
+
var PKG_VERSION = true ? "0.1.119" : "dev";
|
|
206774
207612
|
var LOCAL_BIN = localBin();
|
|
206775
207613
|
var NPM_PREFIX = npmPrefix();
|
|
206776
207614
|
var INSTANCE = DEFAULT_INSTANCE;
|
|
@@ -206852,9 +207690,8 @@ function ensureRuntimeAssets(targetDir = BIN_DIR) {
|
|
|
206852
207690
|
console.error(`[oasis] connector runtime assets: \u4ECE ${synced.from} \u540C\u6B65\u4E86 ${synced.copied} \u4E2A .sh \u5230 ${targetDir}`);
|
|
206853
207691
|
}
|
|
206854
207692
|
}
|
|
206855
|
-
function installSoftware() {
|
|
207693
|
+
function installSoftware(label) {
|
|
206856
207694
|
const src = process.argv[1];
|
|
206857
|
-
const label = versionLabel(src, PKG_VERSION, NPM_PREFIX);
|
|
206858
207695
|
const vDir = versionDir(label);
|
|
206859
207696
|
installBinaryAtomic(src, path32.join(vDir, "oasis.js"));
|
|
206860
207697
|
ensureRuntimeAssets(vDir);
|
|
@@ -206885,27 +207722,13 @@ function setupAutostart(name) {
|
|
|
206885
207722
|
fs38.mkdirSync(unitDir, { recursive: true });
|
|
206886
207723
|
const unit = systemdUnitName(name);
|
|
206887
207724
|
fs38.mkdirSync(path32.dirname(inst.logFile), { recursive: true });
|
|
206888
|
-
fs38.writeFileSync(path32.join(unitDir, unit),
|
|
206889
|
-
|
|
206890
|
-
|
|
206891
|
-
|
|
206892
|
-
|
|
206893
|
-
|
|
206894
|
-
|
|
206895
|
-
// (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
|
|
206896
|
-
"[Service]",
|
|
206897
|
-
`ExecStart=${cmd}`,
|
|
206898
|
-
`Environment=PATH=${daemonPath}`,
|
|
206899
|
-
`Environment=OASIS_CLI_HOME=${inst.currentLink}`,
|
|
206900
|
-
`StandardOutput=append:${inst.logFile}`,
|
|
206901
|
-
`StandardError=append:${inst.logFile}`,
|
|
206902
|
-
"Restart=on-failure",
|
|
206903
|
-
"RestartSec=5",
|
|
206904
|
-
"",
|
|
206905
|
-
"[Install]",
|
|
206906
|
-
"WantedBy=default.target",
|
|
206907
|
-
""
|
|
206908
|
-
].join("\n"));
|
|
207725
|
+
fs38.writeFileSync(path32.join(unitDir, unit), systemdUnitText({
|
|
207726
|
+
name,
|
|
207727
|
+
cmd,
|
|
207728
|
+
daemonPath,
|
|
207729
|
+
cliHome: inst.currentLink,
|
|
207730
|
+
logFile: inst.logFile
|
|
207731
|
+
}));
|
|
206909
207732
|
if (name === DEFAULT_INSTANCE) {
|
|
206910
207733
|
try {
|
|
206911
207734
|
(0, import_node_child_process19.execSync)(`systemctl --user disable --now ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
|
|
@@ -206992,20 +207815,12 @@ function stopDaemon(name) {
|
|
|
206992
207815
|
}
|
|
206993
207816
|
}
|
|
206994
207817
|
}
|
|
206995
|
-
function selfUpdate(
|
|
207818
|
+
function selfUpdate() {
|
|
206996
207819
|
const cfg = readConfig();
|
|
206997
207820
|
if (!cfg?.serverUrl) throw new Error("no config \u2014 run `oasis daemon start` first");
|
|
206998
|
-
if (cfg.pinnedVersion && !opts.force) {
|
|
206999
|
-
console.error(`[oasis] \u5B9E\u4F8B ${INSTANCE} \u5DF2\u9489\u5728 ${cfg.pinnedVersion}\uFF0C\u8DF3\u8FC7\u81EA\u66F4\u65B0\uFF08\u8981\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
|
|
207000
|
-
return false;
|
|
207001
|
-
}
|
|
207002
|
-
if (cfg.pinnedVersion && opts.force) {
|
|
207003
|
-
saveConfig({ ...cfg, pinnedVersion: void 0 });
|
|
207004
|
-
console.error(`[oasis] --force\uFF1A\u5DF2\u89E3\u9664 ${INSTANCE} \u7684\u7248\u672C\u9489\u6B7B\uFF08\u539F ${cfg.pinnedVersion}\uFF09`);
|
|
207005
|
-
}
|
|
207006
207821
|
const nameArg = cfg.name ? ` --name '${cfg.name.replace(/'/g, "'\\''")}'` : "";
|
|
207007
207822
|
const oasisBin = path32.join(NPM_PREFIX, "bin", "oasis");
|
|
207008
|
-
const startCmd = `"${oasisBin}" daemon start --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
|
|
207823
|
+
const startCmd = `"${oasisBin}" daemon start --upgrade --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
|
|
207009
207824
|
const OFFICIAL = "https://registry.npmjs.org/";
|
|
207010
207825
|
const installOfficial = `npm install -g --prefer-online --registry '${OFFICIAL}' --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
|
|
207011
207826
|
const installFallback = `npm install -g --prefer-online --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
|
|
@@ -207030,7 +207845,6 @@ function selfUpdate(opts = {}) {
|
|
|
207030
207845
|
const child = (0, import_node_child_process19.spawn)("sh", ["-c", inner], { detached: true, stdio: ["ignore", log3, log3] });
|
|
207031
207846
|
child.unref();
|
|
207032
207847
|
}
|
|
207033
|
-
return true;
|
|
207034
207848
|
}
|
|
207035
207849
|
function resolveInstance(explicit) {
|
|
207036
207850
|
if (!explicit) return DEFAULT_INSTANCE;
|
|
@@ -207040,6 +207854,103 @@ function resolveInstance(explicit) {
|
|
|
207040
207854
|
}
|
|
207041
207855
|
return explicit;
|
|
207042
207856
|
}
|
|
207857
|
+
function currentLabelOf(inst) {
|
|
207858
|
+
try {
|
|
207859
|
+
return path32.basename(fs38.realpathSync(inst.currentLink));
|
|
207860
|
+
} catch {
|
|
207861
|
+
return null;
|
|
207862
|
+
}
|
|
207863
|
+
}
|
|
207864
|
+
function instanceHasSoftware(inst) {
|
|
207865
|
+
return fs38.existsSync(inst.binFile) && currentLabelOf(inst) !== null;
|
|
207866
|
+
}
|
|
207867
|
+
function newInstanceName() {
|
|
207868
|
+
const existing = new Set(listInstances());
|
|
207869
|
+
if (!existing.has(DEFAULT_INSTANCE)) return DEFAULT_INSTANCE;
|
|
207870
|
+
for (; ; ) {
|
|
207871
|
+
const n = `inst-${(0, import_node_crypto52.randomBytes)(3).toString("hex")}`;
|
|
207872
|
+
if (!existing.has(n)) return n;
|
|
207873
|
+
}
|
|
207874
|
+
}
|
|
207875
|
+
function planStart(explicit, serverUrl) {
|
|
207876
|
+
if (explicit) {
|
|
207877
|
+
const name = resolveInstance(explicit);
|
|
207878
|
+
return { mode: fs38.existsSync(instancePaths(name).configFile) ? "resume" : "new", names: [name] };
|
|
207879
|
+
}
|
|
207880
|
+
const all = listInstances();
|
|
207881
|
+
if (all.length === 0) return { mode: "new", names: [DEFAULT_INSTANCE] };
|
|
207882
|
+
const want = normalizeServerUrl(serverUrl);
|
|
207883
|
+
const hits = all.filter((n) => {
|
|
207884
|
+
const c = readConfigAt(instancePaths(n).configFile);
|
|
207885
|
+
return !!c?.serverUrl && normalizeServerUrl(c.serverUrl) === want;
|
|
207886
|
+
});
|
|
207887
|
+
if (hits.length === 0) return { mode: "new", names: [newInstanceName()] };
|
|
207888
|
+
return { mode: "resume", names: hits };
|
|
207889
|
+
}
|
|
207890
|
+
async function startOneInstance(o) {
|
|
207891
|
+
const { serverUrl, upgrade, flags } = o;
|
|
207892
|
+
const prev = readConfig();
|
|
207893
|
+
if (prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
|
|
207894
|
+
console.error(`[oasis] \u26A0 \u5B9E\u4F8B "${INSTANCE}" \u539F\u672C\u8FDE ${prev.serverUrl}\uFF0C\u672C\u6B21\u5C06\u6539\u8FDE ${serverUrl}\u2014\u2014\u8FD9\u662F**\u8FC1\u79FB**\u4E0D\u662F\u65B0\u589E\u3002`);
|
|
207895
|
+
console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
|
|
207896
|
+
console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
|
|
207897
|
+
}
|
|
207898
|
+
const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
|
|
207899
|
+
let nodeId = serverChanged ? void 0 : prev?.nodeId;
|
|
207900
|
+
let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
|
|
207901
|
+
const enrollToken = o.mode === "new" ? flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"] : void 0;
|
|
207902
|
+
const derivedNodeId = derivedNodeIdFor(INSTANCE);
|
|
207903
|
+
if (enrollToken) {
|
|
207904
|
+
const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
|
|
207905
|
+
method: "POST",
|
|
207906
|
+
headers: { "content-type": "application/json" },
|
|
207907
|
+
body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
|
|
207908
|
+
});
|
|
207909
|
+
if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
|
|
207910
|
+
const body = await res.json();
|
|
207911
|
+
token = body.token;
|
|
207912
|
+
nodeId = body.nodeId;
|
|
207913
|
+
} else if (prev?.nodeId && prev?.token && !serverChanged) {
|
|
207914
|
+
nodeId = prev.nodeId;
|
|
207915
|
+
token = prev.token;
|
|
207916
|
+
}
|
|
207917
|
+
if (!token || !nodeId) {
|
|
207918
|
+
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
|
|
207919
|
+
process.exit(1);
|
|
207920
|
+
}
|
|
207921
|
+
const name = o.applyName ? flags.get("name") ?? prev?.name : prev?.name;
|
|
207922
|
+
const nameChanged = prev?.name !== void 0 && name !== prev.name;
|
|
207923
|
+
saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
|
|
207924
|
+
const before = currentLabelOf(INST);
|
|
207925
|
+
let label = before;
|
|
207926
|
+
if (!instanceHasSoftware(INST) || upgrade) {
|
|
207927
|
+
const next = versionLabel(process.argv[1], PKG_VERSION, NPM_PREFIX);
|
|
207928
|
+
if (before && isLocalLabel(before) && before !== next) {
|
|
207929
|
+
console.log(` \u26A0 \u5373\u5C06\u7528 ${next} \u8986\u76D6\u672C\u5B9E\u4F8B\u6B63\u5728\u8DD1\u7684\u672C\u5730\u6784\u5EFA ${before}`);
|
|
207930
|
+
}
|
|
207931
|
+
label = installSoftware(next);
|
|
207932
|
+
} else {
|
|
207933
|
+
fs38.mkdirSync(path32.dirname(LOCAL_BIN), { recursive: true });
|
|
207934
|
+
fs38.writeFileSync(LOCAL_BIN, shimScript(), { mode: 493 });
|
|
207935
|
+
}
|
|
207936
|
+
const pidNow = readPid();
|
|
207937
|
+
const alive = !!pidNow && isRunning2(pidNow);
|
|
207938
|
+
if (alive && label === before && !serverChanged && !nameChanged) {
|
|
207939
|
+
console.log(`\u2713 ${INSTANCE}\uFF1A\u5DF2\u5728\u8DD1\uFF08PID ${pidNow}\uFF0C${label ?? "?"}\uFF09\uFF0C\u7248\u672C\u4E0E\u914D\u7F6E\u5747\u672A\u53D8\uFF0C\u672A\u91CD\u542F`);
|
|
207940
|
+
return;
|
|
207941
|
+
}
|
|
207942
|
+
stopDaemon(INSTANCE);
|
|
207943
|
+
const tookOver = setupAutostart(INSTANCE);
|
|
207944
|
+
const suffix = nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : "";
|
|
207945
|
+
if (!tookOver) {
|
|
207946
|
+
const pid = spawnDaemon();
|
|
207947
|
+
console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
|
|
207948
|
+
} else {
|
|
207949
|
+
console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
|
|
207950
|
+
}
|
|
207951
|
+
console.log(` instance: ${INSTANCE} nodeId: ${nodeId} version: ${label ?? "?"} config: ${INST.configFile}`);
|
|
207952
|
+
console.log(` logs: tail -f ${LOG_FILE}`);
|
|
207953
|
+
}
|
|
207043
207954
|
function derivedNodeIdFor(name) {
|
|
207044
207955
|
if (name === DEFAULT_INSTANCE) return resolveNodeId();
|
|
207045
207956
|
const user = (() => {
|
|
@@ -207087,19 +207998,32 @@ void (async () => {
|
|
|
207087
207998
|
}
|
|
207088
207999
|
const cleanup = () => {
|
|
207089
208000
|
try {
|
|
207090
|
-
fs38.unlinkSync(PID_FILE);
|
|
208001
|
+
if (fs38.readFileSync(PID_FILE, "utf8").trim() === String(process.pid)) fs38.unlinkSync(PID_FILE);
|
|
207091
208002
|
} catch {
|
|
207092
208003
|
}
|
|
207093
208004
|
};
|
|
207094
208005
|
process.on("exit", cleanup);
|
|
207095
|
-
|
|
207096
|
-
|
|
207097
|
-
|
|
207098
|
-
|
|
207099
|
-
|
|
207100
|
-
|
|
207101
|
-
|
|
207102
|
-
|
|
208006
|
+
let drainNode = null;
|
|
208007
|
+
let shuttingDown = false;
|
|
208008
|
+
const shutdown = (signal) => {
|
|
208009
|
+
if (shuttingDown) {
|
|
208010
|
+
console.error(`[oasis] \u6536\u5230\u7B2C\u4E8C\u6B21 ${signal}\uFF1A\u653E\u5F03\u6392\u7A7A\uFF0C\u7ACB\u5373\u9000\u51FA`);
|
|
208011
|
+
cleanup();
|
|
208012
|
+
process.exit(0);
|
|
208013
|
+
}
|
|
208014
|
+
shuttingDown = true;
|
|
208015
|
+
void (async () => {
|
|
208016
|
+
try {
|
|
208017
|
+
await drainNode?.(signal);
|
|
208018
|
+
} catch (e) {
|
|
208019
|
+
console.error("[oasis] \u505C\u673A\u6392\u7A7A\u5931\u8D25\uFF08\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u53EF\u80FD\u6CA1\u53D1\u51FA\u53BB\uFF09:", String(e));
|
|
208020
|
+
}
|
|
208021
|
+
cleanup();
|
|
208022
|
+
process.exit(0);
|
|
208023
|
+
})();
|
|
208024
|
+
};
|
|
208025
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
208026
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
207103
208027
|
const cleanPath = sanitizeDaemonPath(process.env["PATH"]);
|
|
207104
208028
|
if (cleanPath !== process.env["PATH"]) {
|
|
207105
208029
|
console.error(`[oasis] daemon PATH \u5DF2\u6E05\u6D17\uFF1A\u5254\u9664 agent \u4F1A\u8BDD\u4E34\u65F6 wrapper \u76EE\u5F55 ${(process.env["PATH"] ?? "").split(":").length - cleanPath.split(":").length} \u6761`);
|
|
@@ -207139,66 +208063,33 @@ void (async () => {
|
|
|
207139
208063
|
} catch (e) {
|
|
207140
208064
|
console.error("[oasis] self-update failed:", String(e));
|
|
207141
208065
|
}
|
|
208066
|
+
},
|
|
208067
|
+
onReady: (ctl) => {
|
|
208068
|
+
drainNode = ctl.drain;
|
|
207142
208069
|
}
|
|
207143
208070
|
});
|
|
207144
208071
|
return;
|
|
207145
208072
|
}
|
|
207146
208073
|
if (dsub === "start") {
|
|
208074
|
+
const explicit = flags.get("instance") || void 0;
|
|
208075
|
+
const upgrade = flags.has("upgrade");
|
|
207147
208076
|
const serverUrlFlag = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"];
|
|
207148
|
-
|
|
207149
|
-
const
|
|
207150
|
-
const serverUrl = serverUrlFlag ?? prev?.serverUrl;
|
|
208077
|
+
const probe = explicit ?? listInstances()[0] ?? DEFAULT_INSTANCE;
|
|
208078
|
+
const serverUrl = serverUrlFlag ?? readConfigAt(instancePaths(probe).configFile)?.serverUrl;
|
|
207151
208079
|
if (!serverUrl) {
|
|
207152
|
-
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
|
|
207153
|
-
process.exit(1);
|
|
207154
|
-
}
|
|
207155
|
-
if (!flags.get("instance") && prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
|
|
207156
|
-
console.error(`[oasis] \u26A0 \u5B9E\u4F8B "${INSTANCE}" \u539F\u672C\u8FDE ${prev.serverUrl}\uFF0C\u672C\u6B21\u5C06\u6539\u8FDE ${serverUrl}\u2014\u2014\u8FD9\u662F**\u8FC1\u79FB**\u4E0D\u662F\u65B0\u589E\u3002`);
|
|
207157
|
-
console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
|
|
207158
|
-
console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
|
|
207159
|
-
}
|
|
207160
|
-
const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
|
|
207161
|
-
let nodeId = serverChanged ? void 0 : prev?.nodeId;
|
|
207162
|
-
let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
|
|
207163
|
-
const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
|
|
207164
|
-
const derivedNodeId = derivedNodeIdFor(INSTANCE);
|
|
207165
|
-
if (enrollToken) {
|
|
207166
|
-
const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
|
|
207167
|
-
method: "POST",
|
|
207168
|
-
headers: { "content-type": "application/json" },
|
|
207169
|
-
body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
|
|
207170
|
-
});
|
|
207171
|
-
if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
|
|
207172
|
-
const body = await res.json();
|
|
207173
|
-
token = body.token;
|
|
207174
|
-
nodeId = body.nodeId;
|
|
207175
|
-
} else if (prev?.nodeId && prev?.token && !serverChanged) {
|
|
207176
|
-
nodeId = prev.nodeId;
|
|
207177
|
-
token = prev.token;
|
|
207178
|
-
}
|
|
207179
|
-
if (!token || !nodeId) {
|
|
207180
|
-
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
|
|
208080
|
+
console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
|
|
207181
208081
|
process.exit(1);
|
|
207182
208082
|
}
|
|
207183
|
-
const
|
|
207184
|
-
|
|
207185
|
-
|
|
207186
|
-
|
|
207187
|
-
|
|
207188
|
-
saveConfig({ ...readConfig(), pinnedVersion: installedLabel });
|
|
207189
|
-
console.log(` \u672C\u5730\u6784\u5EFA \u2192 \u5DF2\u9489\u7248\u672C ${installedLabel}\uFF08\u81EA\u66F4\u65B0\u5BF9\u672C\u5B9E\u4F8B\u5931\u6548\uFF1B\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
|
|
208083
|
+
const plan = planStart(explicit, serverUrl);
|
|
208084
|
+
if (plan.mode === "resume" && plan.names.length > 1) {
|
|
208085
|
+
console.log(`\u2192 \u672C\u673A\u6709 ${plan.names.length} \u4E2A\u5B9E\u4F8B\u8FDE\u7740\u8BE5\u670D\u52A1\u7AEF\uFF0C\u9010\u4E2A\u6062\u590D\uFF1A${plan.names.join(", ")}`);
|
|
208086
|
+
} else if (plan.mode === "new" && !explicit && listInstances().length > 0) {
|
|
208087
|
+
console.log(`\u2192 \u672C\u673A\u5DF2\u6709\u5B9E\u4F8B\uFF08${listInstances().join(", ")}\uFF09\uFF0C\u4F46\u90FD\u4E0D\u8FDE\u8BE5\u670D\u52A1\u7AEF\uFF1B\u65B0\u5EFA\u5B9E\u4F8B ${plan.names[0]}`);
|
|
207190
208088
|
}
|
|
207191
|
-
|
|
207192
|
-
|
|
207193
|
-
|
|
207194
|
-
if (!tookOver) {
|
|
207195
|
-
const pid = spawnDaemon();
|
|
207196
|
-
console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
|
|
207197
|
-
} else {
|
|
207198
|
-
console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
|
|
208089
|
+
for (const name of plan.names) {
|
|
208090
|
+
useInstance(name);
|
|
208091
|
+
await startOneInstance({ mode: plan.mode, serverUrl, upgrade, flags, applyName: plan.names.length === 1 });
|
|
207199
208092
|
}
|
|
207200
|
-
console.log(` instance: ${INSTANCE} nodeId: ${nodeId} config: ${INST.configFile}`);
|
|
207201
|
-
console.log(` logs: tail -f ${LOG_FILE}`);
|
|
207202
208093
|
if (!process.env["PATH"]?.includes(path32.dirname(LOCAL_BIN)))
|
|
207203
208094
|
console.log(` add to PATH: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc`);
|
|
207204
208095
|
return;
|
|
@@ -207226,13 +208117,13 @@ void (async () => {
|
|
|
207226
208117
|
console.error("no config \u2014 run `oasis daemon start` first");
|
|
207227
208118
|
process.exit(1);
|
|
207228
208119
|
}
|
|
207229
|
-
const
|
|
207230
|
-
|
|
207231
|
-
|
|
207232
|
-
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}`);
|
|
207233
|
-
} else {
|
|
207234
|
-
console.log(`\uFF08\u672A\u89E6\u53D1\u66F4\u65B0\u2014\u2014\u672C\u5B9E\u4F8B\u5DF2\u9489\u7248\u672C\uFF09`);
|
|
208120
|
+
const cur = currentLabelOf(INST);
|
|
208121
|
+
if (cur && isLocalLabel(cur)) {
|
|
208122
|
+
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`);
|
|
207235
208123
|
}
|
|
208124
|
+
console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
|
|
208125
|
+
selfUpdate();
|
|
208126
|
+
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}`);
|
|
207236
208127
|
return;
|
|
207237
208128
|
}
|
|
207238
208129
|
if (dsub === "status") {
|