@sokel-dev/plugin-sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,9 @@
1
- /**
2
- * 事件源:插件主动把外部事件推给平台起 workflow(协议 §7)。
3
- *
4
- * 与操作的区别:操作是 request/reply(平台调插件),事件是 fire-and-forget(插件推平台)。
5
- *
6
- * 多 bot 单实例(协议 v1.3):平台每次注册/心跳下发「分配给本副本的凭证子集」,
7
- * supervisor 按它 reconcile —— 每个凭证一套源实例,凭证被移除就取消,字段变了就重启。
8
- */
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
9
3
  import { toVars } from "./runtime.js";
10
4
  export const TRIGGER_SUBJECT = "sokel.trigger";
11
5
  export const CREDENTIAL_UPDATE_SUBJECT = "sokel.credential.update";
12
- /** 注册回包 credentials 列表项 —— 分配给本副本的一个 bot 身份。 */
6
+ /** One entry of the registration reply's credentials list — a bot identity assigned here. */
13
7
  export class CredEntry {
14
8
  id;
15
9
  fields;
@@ -17,7 +11,7 @@ export class CredEntry {
17
11
  this.id = id;
18
12
  this.fields = fields;
19
13
  }
20
- /** 字段的稳定签名:reconcile 据此判定「字段变更 → 重启该源实例」。 */
14
+ /** A stable signature of the fields; reconcile uses it to decide "fields changed -> restart". */
21
15
  sig() {
22
16
  return Object.keys(this.fields)
23
17
  .sort()
@@ -25,7 +19,8 @@ export class CredEntry {
25
19
  .join("\n");
26
20
  }
27
21
  }
28
- /** 源实例运行态(源 × 凭证)。随注册/心跳上报,面板据此展示每个 bot。 */
22
+ /** Runtime state per source × credential. Reported with each registration/heartbeat so the panel
23
+ * can show every bot. */
29
24
  export class StateBoard {
30
25
  now;
31
26
  m = new Map();
@@ -41,10 +36,11 @@ export class StateBoard {
41
36
  this.m.set(`${sourceId}|${credId}`, st);
42
37
  }
43
38
  /**
44
- * 只在该实例仍是 running 时改写。
39
+ * Overwrite only while the instance is still `running`.
45
40
  *
46
- * 源自报过状态(如 auth_required)之后正常返回,收尾时不该把那句话盖掉——
47
- * 盖掉之后面板上只剩一个「已退出」,而「为什么退出」正是要看的那一半。
41
+ * A source that reported its own status (auth_required, say) and then returned normally should not
42
+ * have that sentence overwritten on the way out: all the panel would show is "exited", and *why*
43
+ * it exited is the half that matters.
48
44
  */
49
45
  setIfRunning(sourceId, credId, status, error = "") {
50
46
  const cur = this.m.get(`${sourceId}|${credId}`);
@@ -63,12 +59,13 @@ export class StateBoard {
63
59
  : a.source_id.localeCompare(b.source_id));
64
60
  }
65
61
  }
66
- /** 常驻事件源 / webhook 的上下文:推事件、读凭证、回写凭证、上传附件、自报状态。 */
62
+ /** The context for a long-running source or a webhook: push events, read and write back
63
+ * credentials, upload attachments, report state. */
67
64
  export class SourceCtx {
68
65
  credential;
69
66
  credentialId;
70
67
  sourceId;
71
- /** stopped:该源实例被 reconcile 停止时置真。长轮询循环 while (!ctx.stopped)。 */
68
+ /** Set to true when reconcile stops this instance. Long-poll loops run `while (!ctx.stopped)`. */
72
69
  stopped = false;
73
70
  token;
74
71
  publish;
@@ -85,20 +82,21 @@ export class SourceCtx {
85
82
  this.board = opts.board;
86
83
  this.files = opts.files;
87
84
  }
88
- /** 凭证按类型化形状读出(与操作侧 Ctx.credentialAs 同义)。 */
85
+ /** Read the credential into a typed shape (same as Ctx.credentialAs on the operation side). */
89
86
  credentialAs() {
90
87
  return this.credential;
91
88
  }
92
89
  /**
93
- * 推一条事件(fire-and-forget)。
90
+ * Push one event (fire-and-forget).
94
91
  *
95
- * event 必须是已声明的事件 id —— 拼错在这里当场报错,而不是变成一条平台侧无人认领的
96
- * 消息(那种失败没有任何症状:插件日志正常,工作流就是不起)。
97
- * eventId 是幂等键,平台按 (plugin, event, eventId) 去重。
92
+ * `event` must be a declared event id: a typo fails here rather than turning into a message nobody
93
+ * on the platform claims. That failure mode has no symptoms — the plugin log looks fine and the
94
+ * workflow simply never starts. `eventId` is the idempotency key; the platform deduplicates on
95
+ * (plugin, event, eventId).
98
96
  */
99
97
  async trigger(event, eventId, payload) {
100
98
  if (this.validEvents.size > 0 && !this.validEvents.has(event)) {
101
- throw new Error(`未声明的事件 "${event}"(先在 sokel.yaml 的 events 里声明)`);
99
+ throw new Error(`undeclared event "${event}" — declare it under events in sokel.yaml`);
102
100
  }
103
101
  const msg = {
104
102
  token: this.token,
@@ -112,18 +110,20 @@ export class SourceCtx {
112
110
  await this.publish(TRIGGER_SUBJECT, encode(msg));
113
111
  }
114
112
  /**
115
- * 把 patch 回写到本实例绑定的平台凭证(会话型凭证运行中刷新用)。
116
- * 平台是唯一凭证存储方,插件本地从不落地凭证。
113
+ * Write a patch back to the credential bound to this instance (how a session-style credential
114
+ * refreshes itself while running).
115
+ * The platform is the only store for credentials; a plugin never persists them locally.
117
116
  */
118
117
  async updateCredential(patch) {
119
118
  if (!this.credentialId)
120
- throw new Error("本源实例未绑定凭证,无可回写目标");
119
+ throw new Error("this source instance has no bound credential to write back to");
121
120
  if (Object.keys(patch).length === 0)
122
121
  return;
123
122
  Object.assign(this.credential, patch);
124
123
  await this.publish(CREDENTIAL_UPDATE_SUBJECT, encode({ token: this.token, credential_id: this.credentialId, patch }));
125
124
  }
126
- /** 自报运行态(如 session 失效 → auth_required),随心跳上报,面板亮「待登录」。 */
125
+ /** Report state (an expired session becomes auth_required); it rides the heartbeat and lights up
126
+ * "needs login" in the panel. */
127
127
  reportStatus(status, msg = "") {
128
128
  this.board?.set(this.sourceId, this.credentialId, status, msg);
129
129
  }
@@ -136,14 +136,14 @@ export class SourceCtx {
136
136
  if (f.data)
137
137
  return f.data;
138
138
  if (!this.files)
139
- throw new Error("文件运行时未就绪");
139
+ throw new Error("file runtime not ready");
140
140
  return this.files.fetch(f);
141
141
  }
142
142
  }
143
143
  function encode(v) {
144
144
  return new TextEncoder().encode(JSON.stringify(v));
145
145
  }
146
- /** per-credential 源实例监督器:按平台下发的凭证集合起停/重启。 */
146
+ /** Per-credential supervisor: starts, stops and restarts instances to match the assigned set. */
147
147
  export class SourceSupervisor {
148
148
  start;
149
149
  running = new Map();
@@ -152,7 +152,7 @@ export class SourceSupervisor {
152
152
  }
153
153
  reconcile(desired) {
154
154
  const want = new Map(desired.map((c) => [c.id, c]));
155
- // 停:不在期望集合,或字段变更(先停后起 = 重启)
155
+ // Stop: not in the desired set, or its fields changed (stop then start = restart)
156
156
  for (const [id, r] of [...this.running]) {
157
157
  const c = want.get(id);
158
158
  if (c && c.sig() === r.sig)
@@ -160,7 +160,7 @@ export class SourceSupervisor {
160
160
  r.stop();
161
161
  this.running.delete(id);
162
162
  }
163
- // 起:期望但未运行
163
+ // Start: desired but not running
164
164
  for (const [id, c] of want) {
165
165
  if (this.running.has(id))
166
166
  continue;
@@ -173,7 +173,8 @@ export class SourceSupervisor {
173
173
  this.running.clear();
174
174
  }
175
175
  }
176
- /** 空(无凭证插件)→ 一个空凭证裸实例,与有凭证时同一条代码路径。 */
176
+ /** Empty (a plugin with no credentials) becomes one bare instance, so both cases take the same
177
+ * code path. */
177
178
  export function desiredSourceCreds(creds) {
178
179
  return creds.length > 0 ? creds : [new CredEntry()];
179
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Sokel plugin SDK for Node.js / TypeScript.
3
3
  *
4
- * 契约在 sokel.yaml 里声明(语言中立),`sokel-gen generate` 生成类型化的接口与注册口;
5
- * 本包提供运行时:注册握手、心跳、调用分发、文件分块、事件触发、webhook、协作式认证。
4
+ * The contract is declared in a language-neutral sokel.yaml; `sokel-gen generate` turns it into
5
+ * typed interfaces and registration functions. This package is the runtime: registration handshake,
6
+ * heartbeat, call dispatch, chunked file transfer, event triggering, webhooks and collaborative
7
+ * authentication.
6
8
  *
7
9
  * ```ts
8
10
  * import { Plugin } from "@sokel-dev/plugin-sdk";
package/dist/src/index.js CHANGED
@@ -1,8 +1,12 @@
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Sokel plugin SDK for Node.js / TypeScript.
3
5
  *
4
- * 契约在 sokel.yaml 里声明(语言中立),`sokel-gen generate` 生成类型化的接口与注册口;
5
- * 本包提供运行时:注册握手、心跳、调用分发、文件分块、事件触发、webhook、协作式认证。
6
+ * The contract is declared in a language-neutral sokel.yaml; `sokel-gen generate` turns it into
7
+ * typed interfaces and registration functions. This package is the runtime: registration handshake,
8
+ * heartbeat, call dispatch, chunked file transfer, event triggering, webhooks and collaborative
9
+ * authentication.
6
10
  *
7
11
  * ```ts
8
12
  * import { Plugin } from "@sokel-dev/plugin-sdk";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAErG,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAErG,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
@@ -1,25 +1,21 @@
1
- /**
2
- * NATS 承载:插件**出站**连 broker(无入站端口、无公网 IP、无防火墙洞)。
3
- *
4
- * 流程与 Go / Python SDK 逐条对齐(协议 §1-§7):发现 → 连接 → 注册(上报契约)→
5
- * 订阅(队列组)→ 心跳续约 → 分发调用 → 优雅下线。事件源插件另有 per-credential supervisor。
6
- */
7
1
  import type { NatsConnection } from "nats";
8
2
  import type { Plugin } from "./plugin.js";
9
3
  import type { FileRuntime, SokelFile } from "./runtime.js";
10
- /** 同组多副本共享队列组 → 每次调用只投一个副本(真·负载均衡)。 */
4
+ /** Replicas of a group share one queue: each call goes to exactly one of them. */
11
5
  export declare const QUEUE_GROUP = "sokel-workers";
12
- /** 回包/流帧自报副本身份。 */
6
+ /** Replies and stream frames report which replica answered. */
13
7
  export declare const INSTANCE_HEADER = "Sokel-Instance";
14
- /** 经同一条 NATS 连接与平台交换文件字节。不要求插件可达平台 HTTP —— 内网插件同样可用。 */
8
+ /** Exchange file bytes with the platform over the same NATS connection. The plugin never needs HTTP
9
+ * access to the platform, so a plugin behind NAT works the same way. */
15
10
  export declare class NatsFiles implements FileRuntime {
16
11
  private readonly nc;
17
12
  private readonly token;
18
13
  constructor(nc: NatsConnection, token: string);
19
14
  fetch(f: SokelFile): Promise<Uint8Array>;
20
- /** 整块字节。走 storeStream —— 分块协议只该有一份实现。 */
15
+ /** Whole bytes. Delegates to storeStream: the chunking protocol should have one implementation. */
21
16
  store(name: string, mime: string, data: Uint8Array): Promise<SokelFile>;
22
- /** **边读边传**,内存占用恒为一个块。平台那侧本来就是逐块写进 blob 的。 */
17
+ /** **Stream while reading**; memory stays at one chunk. The platform already writes into blob
18
+ * storage chunk by chunk. */
23
19
  storeStream(name: string, mime: string, src: AsyncIterable<Uint8Array>): Promise<SokelFile>;
24
20
  private putChunk;
25
21
  }
@@ -28,12 +24,14 @@ export declare class NatsTransport {
28
24
  private dispatch;
29
25
  }
30
26
  /**
31
- * 统一 https 端点 → 经平台 /connect-info 发现真实承载地址。
32
- * 直填 nats:// / tls:// 时跳过发现(本地开发 / 离线场景)。
27
+ * A single https endpoint becomes the real transport address via the platform's /connect-info.
28
+ * A literal nats:// or tls:// URL skips discovery (local development, offline setups).
33
29
  */
34
30
  export declare function discover(endpoint: string, token: string): Promise<string>;
35
31
  /**
36
- * 副本的稳定身份:重启复用,否则平台侧每次重启都多出一行永远 offline 的幽灵实例。
37
- * 1) SOKEL_INSTANCE_ID;2) 工作目录里按 token 指纹命名的落盘文件;3) 落盘失败 → host-pid。
32
+ * A stable replica identity, reused across restarts. Without it every restart leaves the platform
33
+ * holding another ghost row that stays offline forever.
34
+ * In order: 1) SOKEL_INSTANCE_ID; 2) a file in the working directory named after the token's
35
+ * fingerprint; 3) if writing that file fails, host-pid.
38
36
  */
39
37
  export declare function stableInstanceId(token: string): string;
package/dist/src/nats.js CHANGED
@@ -1,8 +1,12 @@
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
- * NATS 承载:插件**出站**连 broker(无入站端口、无公网 IP、无防火墙洞)。
4
+ * The NATS transport: the plugin **dials out** to the broker (no inbound port, no public IP, no
5
+ * firewall hole).
3
6
  *
4
- * 流程与 Go / Python SDK 逐条对齐(协议 §1-§7):发现 → 连接 → 注册(上报契约)→
5
- * 订阅(队列组)→ 心跳续约 → 分发调用 → 优雅下线。事件源插件另有 per-credential supervisor。
7
+ * The flow matches the Go and Python SDKs step for step (protocol §1-§7): discover, connect,
8
+ * register (reporting the contract), subscribe (as a queue group), renew by heartbeat, dispatch
9
+ * calls, shut down gracefully. Event-source plugins additionally run a per-credential supervisor.
6
10
  */
7
11
  import { connect, headers as natsHeaders } from "nats";
8
12
  import { hostname } from "node:os";
@@ -11,11 +15,11 @@ import { createHash, randomBytes } from "node:crypto";
11
15
  import { OP_WEBHOOK } from "./contract.js";
12
16
  import { env } from "./env.js";
13
17
  import { CredEntry, SourceCtx, SourceSupervisor, desiredSourceCreds } from "./events.js";
14
- /** 同组多副本共享队列组 → 每次调用只投一个副本(真·负载均衡)。 */
18
+ /** Replicas of a group share one queue: each call goes to exactly one of them. */
15
19
  export const QUEUE_GROUP = "sokel-workers";
16
- /** 回包/流帧自报副本身份。 */
20
+ /** Replies and stream frames report which replica answered. */
17
21
  export const INSTANCE_HEADER = "Sokel-Instance";
18
- /** 1 MiB:字节不走操作 reply(受 max_payload 约束),走专用分块通道。 */
22
+ /** 1 MiB. Bytes never ride the operation reply (max_payload); they go through the chunk channel. */
19
23
  const FILE_CHUNK = 1 << 20;
20
24
  const HEARTBEAT_MS = 20_000;
21
25
  const RETRY_MS = 8_000;
@@ -23,7 +27,8 @@ const REQUEST_TIMEOUT_MS = 8_000;
23
27
  const FILE_TIMEOUT_MS = 30_000;
24
28
  const enc = new TextEncoder();
25
29
  const dec = new TextDecoder();
26
- /** 经同一条 NATS 连接与平台交换文件字节。不要求插件可达平台 HTTP —— 内网插件同样可用。 */
30
+ /** Exchange file bytes with the platform over the same NATS connection. The plugin never needs HTTP
31
+ * access to the platform, so a plugin behind NAT works the same way. */
27
32
  export class NatsFiles {
28
33
  nc;
29
34
  token;
@@ -34,7 +39,7 @@ export class NatsFiles {
34
39
  async fetch(f) {
35
40
  const id = f.id || (f.url ? f.url.split("/").pop() : "");
36
41
  if (!id)
37
- throw new Error("文件引用缺少 id/url");
42
+ throw new Error("the file reference has neither id nor url");
38
43
  const chunks = [];
39
44
  for (let seq = 0;; seq++) {
40
45
  const resp = await this.nc.request("sokel.file.get", enc.encode(JSON.stringify({ token: this.token, id, seq })), { timeout: FILE_TIMEOUT_MS });
@@ -46,19 +51,20 @@ export class NatsFiles {
46
51
  return Buffer.concat(chunks);
47
52
  }
48
53
  }
49
- /** 整块字节。走 storeStream —— 分块协议只该有一份实现。 */
54
+ /** Whole bytes. Delegates to storeStream: the chunking protocol should have one implementation. */
50
55
  async store(name, mime, data) {
51
56
  return this.storeStream(name, mime, (async function* () { yield data; })());
52
57
  }
53
- /** **边读边传**,内存占用恒为一个块。平台那侧本来就是逐块写进 blob 的。 */
58
+ /** **Stream while reading**; memory stays at one chunk. The platform already writes into blob
59
+ * storage chunk by chunk. */
54
60
  async storeStream(name, mime, src) {
55
61
  let uploadId = "";
56
62
  let seq = 0;
57
63
  let pending = Buffer.alloc(0);
58
64
  let done = false;
59
65
  const it = src[Symbol.asyncIterator]();
60
- // 攒够一块再发:上游给的分片大小由它自己定(fs 流默认 64KB),
61
- // 直接照发的话块数会翻十几倍,每块都是一次 request-reply。
66
+ // Fill a whole chunk before sending: the source decides its own slice size (a fs stream gives
67
+ // 64KB by default), and forwarding those as-is would multiply the round trips more than tenfold.
62
68
  while (!done) {
63
69
  while (pending.length < FILE_CHUNK) {
64
70
  const r = await it.next();
@@ -76,12 +82,12 @@ export class NatsFiles {
76
82
  uploadId = f.uploadId;
77
83
  if (last) {
78
84
  if (!f.file)
79
- throw new Error("平台未返回文件引用");
85
+ throw new Error("the platform returned no file reference");
80
86
  return f.file;
81
87
  }
82
88
  seq += 1;
83
89
  }
84
- throw new Error("上传未收到末块应答(不该发生)");
90
+ throw new Error("the upload never got a final-chunk reply (should not happen)");
85
91
  }
86
92
  async putChunk(name, mime, uploadId, seq, last, chunk) {
87
93
  {
@@ -106,16 +112,17 @@ export class NatsFiles {
106
112
  export class NatsTransport {
107
113
  async run(p) {
108
114
  const target = await discover(p.endpoint, p.token);
109
- // broker 的传输层鉴权优先用 SOKEL_NATS_TOKEN;缺省回退接入 token(无鉴权 broker 会忽略它)
115
+ // Transport-level auth prefers SOKEL_NATS_TOKEN and falls back to the access token; a broker
116
+ // without auth ignores it either way.
110
117
  const token = env("NATS_TOKEN") || p.token;
111
118
  const ca = env("NATS_CA");
112
119
  const nc = await connectForever({
113
120
  servers: [target],
114
121
  name: p.name,
115
122
  token: token || undefined,
116
- maxReconnectAttempts: -1, // 无限重连;订阅在重连后自动恢复
123
+ maxReconnectAttempts: -1, // reconnect forever; subscriptions restore themselves
117
124
  reconnectTimeWait: 2_000,
118
- waitOnFirstConnect: true, // broker 未就绪时挂起等它,而不是启动失败退出
125
+ waitOnFirstConnect: true, // wait for a broker that is not up yet instead of exiting
119
126
  ...(ca ? { tls: { caFile: ca } } : {}),
120
127
  });
121
128
  const host = hostname();
@@ -130,17 +137,18 @@ export class NatsTransport {
130
137
  });
131
138
  const reg = JSON.parse(dec.decode(resp.data));
132
139
  if (!reg.ok || !reg.subject)
133
- throw new Error(`注册被拒:${reg.error ?? "平台未给 subject"}`);
140
+ throw new Error(`registration refused: ${reg.error ?? "the platform returned no subject"}`);
134
141
  if (reg.notify_subject)
135
142
  notifySubject = reg.notify_subject;
136
143
  let creds = (reg.credentials ?? []).map((c) => new CredEntry(c.id ?? "", c.fields ?? {}));
137
- // 旧平台只有单数形态 → 折算成单元素集合(行为与旧版一致)
144
+ // An older platform only sends the singular form; fold it into a one-element set.
138
145
  if (creds.length === 0 && (reg.credential_id || reg.credential)) {
139
146
  creds = [new CredEntry(reg.credential_id ?? "", reg.credential ?? {})];
140
147
  }
141
148
  return { subject: reg.subject, name: reg.name || p.name, creds };
142
149
  };
143
- // 启动注册失败不退出(broker / 平台可能尚未就绪),固定间隔重试直到成功
150
+ // A failed first registration is not fatal (broker or platform may still be starting): retry at
151
+ // a fixed interval until it succeeds.
144
152
  let first;
145
153
  for (;;) {
146
154
  try {
@@ -148,7 +156,7 @@ export class NatsTransport {
148
156
  break;
149
157
  }
150
158
  catch (e) {
151
- console.warn(`[sokel] 注册失败(${errText(e)}),${RETRY_MS / 1000}s 后重试…`);
159
+ console.warn(`[sokel] registration failed (${errText(e)}), retrying in ${RETRY_MS / 1000}s…`);
152
160
  await sleep(RETRY_MS);
153
161
  }
154
162
  }
@@ -158,20 +166,21 @@ export class NatsTransport {
158
166
  void this.dispatch(p, nc, msg, files, instanceId);
159
167
  }
160
168
  })();
161
- console.log(`[sokel] 已接入平台:插件「${first.name}」就绪,副本 ${instanceId} 监听 ${first.subject}`);
169
+ console.log(`[sokel] connected: plugin "${first.name}" ready, replica ${instanceId} listening on ${first.subject}`);
162
170
  let supervisor;
163
171
  if (p.sources.length > 0) {
164
172
  supervisor = makeSupervisor(p, nc, files);
165
173
  supervisor.reconcile(desiredSourceCreds(first.creds));
166
174
  if (notifySubject) {
167
- // 凭证变更即时通知:普通订阅(非队列组),组内每个副本都要收——各自的分配集合都可能变
175
+ // Credential-change notifications use a plain subscription (not a queue group): every
176
+ // replica in the group must hear it, since any assignment may have changed.
168
177
  const debounced = debounce(300, async () => {
169
178
  try {
170
179
  const { creds } = await register();
171
180
  supervisor.reconcile(desiredSourceCreds(creds));
172
181
  }
173
182
  catch (e) {
174
- console.warn(`[sokel] 凭证变更 re-register 失败: ${errText(e)}`);
183
+ console.warn(`[sokel] re-register after a credential change failed: ${errText(e)}`);
175
184
  }
176
185
  });
177
186
  const notifySub = nc.subscribe(notifySubject);
@@ -181,17 +190,19 @@ export class NatsTransport {
181
190
  })();
182
191
  }
183
192
  }
184
- // 心跳续约保持副本在线;SIGINT/SIGTERM(docker stop / Ctrl-C)→ 优雅下线:
185
- // 通知平台立即标记 offline(秒级感知),而非等心跳超时清扫(45s+)
193
+ // The heartbeat keeps the replica online. On SIGINT/SIGTERM (docker stop, Ctrl-C) it shuts down
194
+ // gracefully: tell the platform to mark this replica offline right away (seconds), instead of
195
+ // waiting for the heartbeat sweep (45s+).
186
196
  await new Promise((resolve) => {
187
197
  const timer = setInterval(async () => {
188
198
  try {
189
199
  const { creds } = await register();
190
- // 每拍按最新分配集合 reconcile:分片迁移 / 凭证增删 / 字段刷新 → 起停或重启源实例
200
+ // Reconcile against the latest assignment each tick: shard moves, credentials added or
201
+ // removed, fields refreshed.
191
202
  supervisor?.reconcile(desiredSourceCreds(creds));
192
203
  }
193
204
  catch (e) {
194
- console.warn(`[sokel] 心跳续约失败: ${errText(e)}`);
205
+ console.warn(`[sokel] heartbeat renewal failed: ${errText(e)}`);
195
206
  }
196
207
  }, HEARTBEAT_MS);
197
208
  const bye = (sig) => {
@@ -199,11 +210,11 @@ export class NatsTransport {
199
210
  supervisor?.stopAll();
200
211
  nc.publish("sokel.unregister", enc.encode(JSON.stringify({ token: p.token, instance_id: instanceId })));
201
212
  void nc
202
- .flush() // 确保下线通知先于断连送达
213
+ .flush() // make sure the goodbye lands before the connection drops
203
214
  .then(() => nc.drain())
204
215
  .catch(() => undefined)
205
216
  .then(() => {
206
- console.log(`[sokel] 收到 ${sig},已通知平台下线,退出`);
217
+ console.log(`[sokel] got ${sig}, told the platform we are going offline, exiting`);
207
218
  resolve();
208
219
  });
209
220
  };
@@ -219,15 +230,16 @@ export class NatsTransport {
219
230
  call = JSON.parse(dec.decode(msg.data));
220
231
  }
221
232
  catch {
222
- nc.publish(msg.reply, enc.encode(JSON.stringify({ error: "调用帧解不开" })));
233
+ nc.publish(msg.reply, enc.encode(JSON.stringify({ error: "could not parse the call frame" })));
223
234
  return;
224
235
  }
225
236
  const op = call.operation ?? "";
226
237
  const tag = traceTag(call.trace ?? {});
227
- // 平台代收 webhook 的特殊帧:分发前拦截(老 SDK 没这段会走 unknown operation,
228
- // 平台把它翻译成「插件未注册 webhook 处理器」)
238
+ // The platform-relayed webhook frame is intercepted before dispatch. An older SDK without this
239
+ // branch falls through to "unknown operation", which the platform translates into "the plugin
240
+ // registered no webhook handler".
229
241
  if (op === OP_WEBHOOK) {
230
- console.log(`[sokel] ← webhook 入站${tag}`);
242
+ console.log(`[sokel] ← webhook inbound${tag}`);
231
243
  const sctx = new SourceCtx({
232
244
  token: p.token,
233
245
  publish: (subject, data) => nc.publish(subject, data),
@@ -244,16 +256,16 @@ export class NatsTransport {
244
256
  const h = natsHeaders();
245
257
  h.set(INSTANCE_HEADER, instanceId);
246
258
  const started = Date.now();
247
- console.log(`[sokel] ← ${op} 开始${tag}`);
259
+ console.log(`[sokel] ← ${op} started${tag}`);
248
260
  if (p.contract.isStream(op)) {
249
- // 流式:逐帧发布到回复通道,末尾必发终止帧
261
+ // Streaming: publish frame by frame to the reply subject; the end frame is mandatory.
250
262
  const publishFrame = (f) => nc.publish(msg.reply, enc.encode(JSON.stringify(f)), { headers: h });
251
263
  try {
252
264
  await p.dispatch(call, publishFrame, files);
253
- console.log(`[sokel] ✓ ${op} 完成(${Date.now() - started}ms)${tag}`);
265
+ console.log(`[sokel] ✓ ${op} done (${Date.now() - started}ms)${tag}`);
254
266
  }
255
267
  catch (e) {
256
- console.warn(`[sokel] ✗ ${op} 失败(${Date.now() - started}ms)${tag}: ${errText(e)}`);
268
+ console.warn(`[sokel] ✗ ${op} failed (${Date.now() - started}ms)${tag}: ${errText(e)}`);
257
269
  publishFrame({ kind: "error", text: errText(e) });
258
270
  }
259
271
  publishFrame({ kind: "end" });
@@ -261,16 +273,17 @@ export class NatsTransport {
261
273
  }
262
274
  try {
263
275
  const vars = await p.dispatchBuffered(call, files);
264
- console.log(`[sokel] ✓ ${op} 完成(${Date.now() - started}ms)${tag}`);
276
+ console.log(`[sokel] ✓ ${op} done (${Date.now() - started}ms)${tag}`);
265
277
  nc.publish(msg.reply, enc.encode(JSON.stringify(vars)), { headers: h });
266
278
  }
267
279
  catch (e) {
268
- console.warn(`[sokel] ✗ ${op} 失败(${Date.now() - started}ms)${tag}: ${errText(e)}`);
280
+ console.warn(`[sokel] ✗ ${op} failed (${Date.now() - started}ms)${tag}: ${errText(e)}`);
269
281
  nc.publish(msg.reply, enc.encode(JSON.stringify({ error: errText(e) })), { headers: h });
270
282
  }
271
283
  }
272
284
  }
273
- /** 每个凭证一套源实例:ctx 绑定该凭证,trigger 自动回带其 credential_id。 */
285
+ /** One source instance per credential: its ctx is bound to that credential, and trigger carries the
286
+ * credential_id back automatically. */
274
287
  function makeSupervisor(p, nc, files) {
275
288
  return new SourceSupervisor((cred) => {
276
289
  const ctxs = [];
@@ -286,7 +299,7 @@ function makeSupervisor(p, nc, files) {
286
299
  files,
287
300
  });
288
301
  ctxs.push(sctx);
289
- console.log(`[sokel] 事件源「${src.id}」启动(credential=${cred.id || "(无凭证)"})`);
302
+ console.log(`[sokel] event source "${src.id}" started (credential=${cred.id || "(none)"})`);
290
303
  p.board.set(src.id, cred.id, "running");
291
304
  void src
292
305
  .fn(sctx)
@@ -296,13 +309,14 @@ function makeSupervisor(p, nc, files) {
296
309
  })
297
310
  .catch((e) => {
298
311
  if (sctx.stopped)
299
- return; // reconcile 停止:状态已由 stop() 整体移除
300
- console.warn(`[sokel] 事件源「${src.id}」退出(credential=${cred.id || "(无凭证)"}): ${errText(e)}`);
312
+ return; // stopped by reconcile: stop() already removed the state
313
+ console.warn(`[sokel] event source "${src.id}" exited (credential=${cred.id || "(none)"}): ${errText(e)}`);
301
314
  p.board.set(src.id, cred.id, "error", errText(e));
302
315
  });
303
316
  }
304
317
  return () => {
305
- // JS 没有任务取消:约定由源循环自己看 ctx.stopped 退出(长轮询在下一拍即感知)
318
+ // JS has no task cancellation: by convention the source loop watches ctx.stopped and exits
319
+ // (a long poll notices on its next tick).
306
320
  for (const c of ctxs)
307
321
  c.stopped = true;
308
322
  p.board.removeCred(cred.id);
@@ -310,29 +324,31 @@ function makeSupervisor(p, nc, files) {
310
324
  });
311
325
  }
312
326
  /**
313
- * 统一 https 端点 → 经平台 /connect-info 发现真实承载地址。
314
- * 直填 nats:// / tls:// 时跳过发现(本地开发 / 离线场景)。
327
+ * A single https endpoint becomes the real transport address via the platform's /connect-info.
328
+ * A literal nats:// or tls:// URL skips discovery (local development, offline setups).
315
329
  */
316
330
  export async function discover(endpoint, token) {
317
331
  const ep = (endpoint ?? "").trim();
318
332
  if (ep.startsWith("nats://") || ep.startsWith("tls://"))
319
333
  return ep;
320
334
  if (!ep.startsWith("http://") && !ep.startsWith("https://")) {
321
- throw new Error(`端点 "${endpoint}" 不合法:应为平台地址(https://…)或 nats://`);
335
+ throw new Error(`invalid endpoint "${endpoint}": expected a platform URL (https://…) or nats://`);
322
336
  }
323
337
  const url = ep.replace(/\/+$/, "") + "/api/v1/connect-info";
324
338
  const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
325
339
  if (!resp.ok)
326
- throw new Error(`平台发现失败 ${url}: HTTP ${resp.status}`);
340
+ throw new Error(`discovery failed at ${url}: HTTP ${resp.status}`);
327
341
  const info = (await resp.json());
328
342
  const target = info.transports?.nats;
329
343
  if (!target)
330
- throw new Error("平台未提供可用承载(connect-info.transports 为空)");
344
+ throw new Error("the platform offers no transport (connect-info.transports is empty)");
331
345
  return target;
332
346
  }
333
347
  /**
334
- * 副本的稳定身份:重启复用,否则平台侧每次重启都多出一行永远 offline 的幽灵实例。
335
- * 1) SOKEL_INSTANCE_ID;2) 工作目录里按 token 指纹命名的落盘文件;3) 落盘失败 → host-pid。
348
+ * A stable replica identity, reused across restarts. Without it every restart leaves the platform
349
+ * holding another ghost row that stays offline forever.
350
+ * In order: 1) SOKEL_INSTANCE_ID; 2) a file in the working directory named after the token's
351
+ * fingerprint; 3) if writing that file fails, host-pid.
336
352
  */
337
353
  export function stableInstanceId(token) {
338
354
  const explicit = env("INSTANCE_ID");
@@ -347,7 +363,7 @@ export function stableInstanceId(token) {
347
363
  return existing;
348
364
  }
349
365
  catch {
350
- /* 首次运行:往下走,生成并落盘 */
366
+ /* first run: fall through, generate one and write it out */
351
367
  }
352
368
  const id = `${hostname()}-${randomBytes(4).toString("hex")}`;
353
369
  try {
@@ -364,7 +380,7 @@ async function connectForever(opts) {
364
380
  return await connect(opts);
365
381
  }
366
382
  catch (e) {
367
- console.warn(`[sokel] 连接平台失败(${errText(e)}),${RETRY_MS / 1000}s 后重试…`);
383
+ console.warn(`[sokel] could not connect to the platform (${errText(e)}), retrying in ${RETRY_MS / 1000}s…`);
368
384
  await sleep(RETRY_MS);
369
385
  }
370
386
  }