lark-remote 0.5.3 → 0.5.5
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/CHANGELOG.md +38 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/bridge/approval-coordinator.d.ts +18 -1
- package/dist/bridge/approval-coordinator.js +57 -10
- package/dist/bridge/index.js +90 -17
- package/dist/card/approval-render.js +18 -0
- package/dist/card/run-state.js +10 -5
- package/dist/config/index.d.ts +16 -0
- package/dist/config/index.js +46 -6
- package/dist/config/opencode-config.d.ts +3 -3
- package/dist/config/opencode-config.js +38 -22
- package/dist/inbound/authorize.d.ts +34 -0
- package/dist/inbound/authorize.js +31 -0
- package/dist/index.js +116 -63
- package/dist/instance-lock.d.ts +9 -0
- package/dist/instance-lock.js +113 -42
- package/dist/router/config/dsh.d.ts +51 -21
- package/dist/router/config/dsh.js +255 -66
- package/dist/router/config/opencode.js +16 -6
- package/dist/router/config/types.d.ts +6 -3
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +13 -5
- package/dist/runner/codex/app-server/translator.js +21 -7
- package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
- package/dist/runner/common/acp/base-acp-runner.js +83 -8
- package/dist/runner/common/acp/protocol-types.d.ts +10 -2
- package/dist/runner/common/acp/protocol-types.js +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +15 -0
- package/dist/runner/common/connection-based-runner.js +40 -5
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
- package/dist/runner/dsh/approval.d.ts +53 -0
- package/dist/runner/dsh/approval.js +161 -0
- package/dist/runner/dsh/auth.d.ts +36 -0
- package/dist/runner/dsh/auth.js +118 -0
- package/dist/runner/dsh/client.d.ts +161 -42
- package/dist/runner/dsh/client.js +551 -244
- package/dist/runner/dsh/daemon.d.ts +85 -0
- package/dist/runner/dsh/daemon.js +272 -0
- package/dist/runner/dsh/merge.d.ts +9 -0
- package/dist/runner/dsh/merge.js +51 -0
- package/dist/runner/dsh/runner.d.ts +94 -11
- package/dist/runner/dsh/runner.js +553 -67
- package/dist/runner/dsh/translator.d.ts +43 -3
- package/dist/runner/dsh/translator.js +115 -38
- package/dist/runner/dsh/types.d.ts +189 -27
- package/dist/runner/dsh/types.js +11 -5
- package/dist/runner/kimi/acp/runner.js +15 -3
- package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
- package/dist/runner/opencode/acp/error-monitor.js +2 -2
- package/dist/runner/opencode/acp/runner.d.ts +9 -0
- package/dist/runner/opencode/acp/runner.js +40 -1
- package/dist/runner/opencode/acp/translator.d.ts +4 -2
- package/dist/runner/opencode/acp/translator.js +12 -3
- package/dist/runner/pi/rpc/translator.d.ts +2 -0
- package/dist/runner/pi/rpc/translator.js +27 -1
- package/dist/runner/types.d.ts +34 -0
- package/dist/runner/types.js +24 -0
- package/dist/session/codex/rollout-reader.js +44 -7
- package/dist/session/dsh/sessions.d.ts +45 -6
- package/dist/session/dsh/sessions.js +171 -27
- package/dist/session/opencode/sessions.d.ts +18 -7
- package/dist/session/opencode/sessions.js +294 -15
- package/dist/session/pi/sessions.js +6 -2
- package/package.json +3 -1
package/dist/instance-lock.js
CHANGED
|
@@ -14,8 +14,20 @@
|
|
|
14
14
|
* 与 killOrphan 的差异:killOrphan 是 fail-closed(身份不明就不杀进程),
|
|
15
15
|
* 这里是 fail-open(身份不明就认定锁还活着,绝不 clobber 无法证伪的锁)。
|
|
16
16
|
* 两者方向相反,因此身份判定取三态而不是布尔。
|
|
17
|
+
*
|
|
18
|
+
* 陈旧锁接管是「原子改名 + 复核」两步:
|
|
19
|
+
* 旧实现 read→isHeldByLive→unlink→create 四个动作不是原子的,两个进程可以
|
|
20
|
+
* 先后 unlink 掉对方刚创建的锁,双双认为自己持锁。现在改成:
|
|
21
|
+
* 1. `rename(lock → quarantine.<pid>.<rand>)`:rename 是原子的,同一时刻
|
|
22
|
+
* 只有一个进程能把当前锁文件「夺走」;
|
|
23
|
+
* 2. 复核夺到的 quarantine 内容——它可能已经不是刚才读到的陈旧锁(原持有者
|
|
24
|
+
* 恰好释放、其它进程刚创建),活的就原地 link 回去并拒绝;
|
|
25
|
+
* 3. 确认陈旧后才 `open(..., 'wx')` 建新锁;拿到 EEXIST 说明别的进程抢先,
|
|
26
|
+
* 重新读取并按「活锁 → 拒绝 / 陈旧 → 下一轮」处理(有限重试 + 退避)。
|
|
27
|
+
* 任何一步失败都不会直接 unlink 当前锁,避免把别人刚建好的锁删掉。
|
|
17
28
|
*/
|
|
18
29
|
import fs from 'node:fs';
|
|
30
|
+
import { randomBytes } from 'node:crypto';
|
|
19
31
|
import path from 'node:path';
|
|
20
32
|
import { bestEffortUnlink } from './common/fs.js';
|
|
21
33
|
import { isWin32 } from './platform/select.js';
|
|
@@ -29,6 +41,15 @@ export class InstanceAlreadyRunningError extends Error {
|
|
|
29
41
|
this.lockPath = lockPath;
|
|
30
42
|
}
|
|
31
43
|
}
|
|
44
|
+
/** 陈旧锁接管的最大轮次(每轮都是「读 → 原子改名 → 复核 → 尝试创建」)。 */
|
|
45
|
+
const MAX_ACQUIRE_ATTEMPTS = 10;
|
|
46
|
+
/** 每轮退避 5–25ms(含抖动),让并发启动的进程错开而不是持续碰撞。 */
|
|
47
|
+
function acquireBackoffMs() {
|
|
48
|
+
return 5 + Math.floor(Math.random() * 20);
|
|
49
|
+
}
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
32
53
|
/**
|
|
33
54
|
* Probe whether a pid is alive.
|
|
34
55
|
* - process.kill(pid, 0) returning normally → alive.
|
|
@@ -71,49 +92,54 @@ export class InstanceLock {
|
|
|
71
92
|
*/
|
|
72
93
|
async acquire() {
|
|
73
94
|
fs.mkdirSync(path.dirname(this.lockPath), { recursive: true });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw new InstanceAlreadyRunningError(pid, this.lockPath);
|
|
88
|
-
}
|
|
89
|
-
// Stale lock: remove and retry once
|
|
90
|
-
bestEffortUnlink(this.lockPath);
|
|
91
|
-
try {
|
|
92
|
-
const fd = fs.openSync(this.lockPath, 'wx');
|
|
93
|
-
fs.writeSync(fd, this.lockContents(process.pid));
|
|
94
|
-
fs.closeSync(fd);
|
|
95
|
-
}
|
|
96
|
-
catch (retryErr) {
|
|
97
|
-
if (retryErr.code === 'EEXIST') {
|
|
98
|
-
// Another process created the lock between unlink and retry.
|
|
99
|
-
// Bounded retry (max 1): give up rather than recurse.
|
|
100
|
-
const { pid, name } = this.readPidAndName();
|
|
101
|
-
if (pid && (await this.isHeldByLiveInstance(pid, name))) {
|
|
102
|
-
throw new InstanceAlreadyRunningError(pid, this.lockPath);
|
|
103
|
-
}
|
|
104
|
-
// New lock is also stale — give up rather than recurse.
|
|
105
|
-
throw new InstanceAlreadyRunningError(pid ?? -1, this.lockPath);
|
|
106
|
-
}
|
|
107
|
-
throw retryErr;
|
|
108
|
-
}
|
|
95
|
+
for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {
|
|
96
|
+
// 1) 原子创建:'wx' 在文件已存在时失败,正常路径一步到位。
|
|
97
|
+
if (this.tryCreate())
|
|
98
|
+
return;
|
|
99
|
+
// 2) 锁已存在:活的就拒绝,无法证伪也拒绝(fail-open)。
|
|
100
|
+
const owner = this.readPidAndName(this.lockPath);
|
|
101
|
+
if (owner.pid && (await this.isHeldByLiveInstance(owner.pid, owner.name))) {
|
|
102
|
+
throw new InstanceAlreadyRunningError(owner.pid, this.lockPath);
|
|
103
|
+
}
|
|
104
|
+
// 3) 判为陈旧:原子地把当前锁文件改名到 quarantine(不直接 unlink)。
|
|
105
|
+
const quarantine = `${this.lockPath}.stale-${process.pid}-${randomBytes(6).toString('hex')}`;
|
|
106
|
+
try {
|
|
107
|
+
fs.renameSync(this.lockPath, quarantine);
|
|
109
108
|
}
|
|
110
|
-
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err.code === 'ENOENT') {
|
|
111
|
+
// 别的进程先一步夺走/释放了锁:重新进入创建竞争。
|
|
112
|
+
await sleep(acquireBackoffMs());
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
111
115
|
throw err;
|
|
112
116
|
}
|
|
117
|
+
// 4) 复核夺到的锁:读→rename 之间可能已被新实例替换成活锁。
|
|
118
|
+
// 内容与刚才读到的陈旧锁一致 = 同一个文件,直接沿用第 2 步的裁决(陈旧),
|
|
119
|
+
// 不重复查身份(win32 上每次身份查询要拉 PowerShell)。
|
|
120
|
+
const stolen = this.readPidAndName(quarantine);
|
|
121
|
+
const unchanged = stolen.pid === owner.pid && stolen.name === owner.name;
|
|
122
|
+
if (!unchanged && stolen.pid && (await this.isHeldByLiveInstance(stolen.pid, stolen.name))) {
|
|
123
|
+
this.restoreQuarantine(quarantine);
|
|
124
|
+
throw new InstanceAlreadyRunningError(stolen.pid, this.lockPath);
|
|
125
|
+
}
|
|
126
|
+
// 5) 确属陈旧:创建新锁。失败 = 别的进程在 rename 与 create 之间抢先。
|
|
127
|
+
const created = this.tryCreate();
|
|
128
|
+
bestEffortUnlink(quarantine);
|
|
129
|
+
if (created)
|
|
130
|
+
return;
|
|
131
|
+
const winner = this.readPidAndName(this.lockPath);
|
|
132
|
+
if (winner.pid && (await this.isHeldByLiveInstance(winner.pid, winner.name))) {
|
|
133
|
+
throw new InstanceAlreadyRunningError(winner.pid, this.lockPath);
|
|
134
|
+
}
|
|
135
|
+
await sleep(acquireBackoffMs());
|
|
113
136
|
}
|
|
137
|
+
// 有限重试耗尽:仍然拿不到,按「有(可能是陈旧的)锁存在」报错,
|
|
138
|
+
// 不递归、不无界重试(并发启动风暴下宁可失败也不双开)。
|
|
139
|
+
throw new InstanceAlreadyRunningError(this.readPidAndName(this.lockPath).pid ?? -1, this.lockPath);
|
|
114
140
|
}
|
|
115
141
|
release() {
|
|
116
|
-
const { pid } = this.readPidAndName();
|
|
142
|
+
const { pid } = this.readPidAndName(this.lockPath);
|
|
117
143
|
if (pid === process.pid) {
|
|
118
144
|
bestEffortUnlink(this.lockPath);
|
|
119
145
|
}
|
|
@@ -151,17 +177,62 @@ export class InstanceLock {
|
|
|
151
177
|
lockContents(pid) {
|
|
152
178
|
return `${pid}\n${binaryName(process.execPath)}`;
|
|
153
179
|
}
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
/** 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。 */
|
|
181
|
+
tryCreate() {
|
|
182
|
+
try {
|
|
183
|
+
const fd = fs.openSync(this.lockPath, 'wx');
|
|
184
|
+
try {
|
|
185
|
+
fs.writeSync(fd, this.lockContents(process.pid));
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
fs.closeSync(fd);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err.code === 'EEXIST')
|
|
194
|
+
return false;
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 把夺来的活锁放回原位。优先 `link`(目标已存在会失败,不会覆盖更新的锁),
|
|
200
|
+
* 再删 quarantine。目标已被更新锁占据时放弃恢复——此时当前锁文件属于一个
|
|
201
|
+
* 活实例,拒绝取锁仍然是对的。
|
|
202
|
+
*/
|
|
203
|
+
restoreQuarantine(quarantine) {
|
|
204
|
+
try {
|
|
205
|
+
fs.linkSync(quarantine, this.lockPath);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// EEXIST:已有更新的锁(不要覆盖);文件系统不支持 hardlink 时目标通常
|
|
209
|
+
// 仍不存在,退化为 rename 恢复(目标已存在则不碰)。
|
|
210
|
+
if (!fs.existsSync(this.lockPath)) {
|
|
211
|
+
try {
|
|
212
|
+
fs.renameSync(quarantine, this.lockPath);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* 放弃恢复:当前锁文件仍可能属于一个活实例,拒绝取锁是对的。 */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
bestEffortUnlink(quarantine);
|
|
220
|
+
}
|
|
221
|
+
/** 读取锁文件内容(无副作用:不再在读取失败时 unlink 当前锁)。 */
|
|
222
|
+
readPidAndName(file) {
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
raw = fs.readFileSync(file, 'utf-8').trim();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
156
228
|
return { pid: undefined, name: undefined };
|
|
157
|
-
|
|
229
|
+
}
|
|
158
230
|
const [pidStr, ...nameParts] = raw.split('\n');
|
|
159
231
|
const pid = Number(pidStr);
|
|
160
232
|
const name = nameParts.join('\n') || undefined;
|
|
161
233
|
if (Number.isInteger(pid) && pid > 0 && name)
|
|
162
234
|
return { pid, name };
|
|
163
|
-
// Malformed or missing name — treat as corrupt
|
|
164
|
-
bestEffortUnlink(this.lockPath);
|
|
235
|
+
// Malformed or missing name — treat as corrupt/stale (caller quarantines it).
|
|
165
236
|
return { pid: undefined, name: undefined };
|
|
166
237
|
}
|
|
167
238
|
/**
|
|
@@ -1,51 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DshConfigBuilder — /config card fields for the DSH agent.
|
|
3
3
|
*
|
|
4
|
-
* DSH (DeepSeek Harness) is an HTTP-only agent.
|
|
5
|
-
*
|
|
4
|
+
* DSH (DeepSeek Harness) is an HTTP-only agent. The card exposes the Web Host
|
|
5
|
+
* base URL plus preset/provider/model/reasoning-effort knobs. The launch token
|
|
6
|
+
* is a YAML-only fallback and is never rendered or saved through the card.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* 预取(Router 调 `prefetch()
|
|
8
|
+
* 清单来源:
|
|
9
|
+
* - 动态清单:打开 /config 前通过 `session/modelCatalog` / `agentPresets/list`
|
|
10
|
+
* 预取(Router 调 `prefetch()`),失败/离线回退固定兜底清单,卡不依赖
|
|
10
11
|
* DSH 服务在线。
|
|
11
|
-
* -
|
|
12
|
-
* 在 handleFieldChange 中被转成 undefined patch(router
|
|
13
|
-
* 不写该键 = 服务端默认。
|
|
12
|
+
* - provider / model / reasoningEffort 用空字符串哨兵表示「跟随默认」:select
|
|
13
|
+
* 的 value `''` 在 handleFieldChange 中被转成 undefined patch(router 删除
|
|
14
|
+
* 键),config.yaml 不写该键 = 服务端默认。
|
|
15
|
+
* - provider 没有哨兵:旧配置没有 provider 时按 `deepseek-official` 生效,
|
|
16
|
+
* 与 runner 的兼容规则一致。provider 清单不硬编码,来自服务端目录。
|
|
14
17
|
*/
|
|
15
18
|
import type { AgentConfigCardBuilder, ConfigField } from './types.js';
|
|
16
19
|
import { type AppConfig } from '../../config/index.js';
|
|
17
|
-
import type { DshModelCatalogValue, DshPresetListValue } from '../../runner/dsh/types.js';
|
|
20
|
+
import type { DshModelCatalogValue, DshPermissionCatalogValue, DshPresetListValue } from '../../runner/dsh/types.js';
|
|
18
21
|
/** DshConfigBuilder 预取只依赖的 client 方法子集。 */
|
|
19
22
|
export interface DshCatalogClient {
|
|
20
23
|
listModels(): Promise<DshModelCatalogValue>;
|
|
21
24
|
listPresets(): Promise<DshPresetListValue>;
|
|
25
|
+
listPermissionPresets(): Promise<DshPermissionCatalogValue>;
|
|
22
26
|
}
|
|
23
27
|
export declare class DshConfigBuilder implements AgentConfigCardBuilder {
|
|
24
|
-
/** 最近一次预取用的 host;host
|
|
28
|
+
/** 最近一次预取用的 host;host 变更时缓存失效。 */
|
|
25
29
|
private prefetchHost;
|
|
26
30
|
private modelCatalog;
|
|
27
31
|
private presets;
|
|
32
|
+
private permissionCatalog;
|
|
28
33
|
/** 可注入 client 工厂(测试用);默认生产实现。 */
|
|
29
34
|
private readonly clientFactory;
|
|
30
|
-
constructor(clientFactory?: (baseUrl: string) => DshCatalogClient);
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
|
|
35
|
+
constructor(clientFactory?: (baseUrl: string, token?: string, credentialsFile?: string) => DshCatalogClient);
|
|
36
|
+
/**
|
|
37
|
+
* 异步预取模型/预设目录。失败/离线静默回退固定兜底清单,卡不依赖 DSH
|
|
38
|
+
* 服务在线。`force` 用于打开 /config 时强制刷新(在 DSH Web UI 新增
|
|
39
|
+
* provider 后不需要重启 lark-remote)。三个请求并行,最坏等待一个超时。
|
|
40
|
+
*/
|
|
41
|
+
prefetch(host?: string, token?: string, credentialsFile?: string, options?: {
|
|
42
|
+
force?: boolean;
|
|
43
|
+
}): Promise<void>;
|
|
34
44
|
/** 同步注入 catalog/presets(测试用,替代异步 prefetch;标记 prefetchHost 使
|
|
35
45
|
* 后续 prefetch 短路,避免测试误触发网络请求)。 */
|
|
36
|
-
setCatalog(catalog: DshModelCatalogValue, presets?: DshPresetListValue): void;
|
|
37
|
-
/**
|
|
38
|
-
private
|
|
46
|
+
setCatalog(catalog: DshModelCatalogValue, presets?: DshPresetListValue, permissionCatalog?: DshPermissionCatalogValue): void;
|
|
47
|
+
/** Provider 下拉:目录 groups + 当前值兜底;离线时只保留当前值。 */
|
|
48
|
+
private providerOptions;
|
|
49
|
+
/**
|
|
50
|
+
* 当前 provider 的模型清单。目录在线但该 provider 不在目录中时返回空列表
|
|
51
|
+
* (不把 DeepSeek 模型冒充成其他 provider 的模型);目录离线时用固定兜底。
|
|
52
|
+
*/
|
|
53
|
+
private modelOptionsFor;
|
|
54
|
+
private modelValues;
|
|
55
|
+
/** Provider 的默认模型:目录默认值属于该 provider 时优先,否则首个模型。 */
|
|
56
|
+
private defaultModelFor;
|
|
57
|
+
/**
|
|
58
|
+
* 模型可用的推理档位。目录在线时严格按模型声明:模型没有 reasoning 元数据
|
|
59
|
+
* 就返回空(只留「跟随模型默认」),避免把 A 模型的档位塞给 B 模型。
|
|
60
|
+
*/
|
|
61
|
+
private effortOptionsFor;
|
|
39
62
|
/** preset 清单;无 presets 时回退固定清单。显示名优先用 preset.yml 的 name。 */
|
|
40
63
|
private presetOptions;
|
|
41
|
-
/**
|
|
42
|
-
private
|
|
64
|
+
/** 权限预设清单;配置默认只允许 defaultOptions(auto 只适用于当前会话)。 */
|
|
65
|
+
private permissionOptions;
|
|
66
|
+
/** “跟随服务端默认”文案带上实际默认值,避免用户不知道默认是什么。 */
|
|
67
|
+
private permissionDefaultText;
|
|
43
68
|
buildFields(displayConfig: AppConfig): ConfigField[];
|
|
44
69
|
handleFieldChange(key: string, value: unknown, config: AppConfig): Array<{
|
|
45
70
|
key: string;
|
|
46
71
|
value: unknown;
|
|
47
72
|
}>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
73
|
+
/** 切 provider 后的联动:provider → 新 provider 的默认/首个模型 → 合法档位。 */
|
|
74
|
+
private providerChangePatches;
|
|
75
|
+
/**
|
|
76
|
+
* 切到 (provider, newModel) 后的档位补丁:当前档位仍被新模型支持 → 不产出;
|
|
77
|
+
* 否则用新模型目录中位;目录离线 → 不产出(保留现状,由服务端校验);
|
|
78
|
+
* 模型无 reasoning 元数据 → 清空档位(跟随模型默认)。
|
|
79
|
+
*/
|
|
50
80
|
private effortPatchForModel;
|
|
51
81
|
}
|