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/config/index.js
CHANGED
|
@@ -130,10 +130,11 @@ export const CodexConfigSchema = z.object({
|
|
|
130
130
|
});
|
|
131
131
|
/** OpenCode-specific configuration (pure ACP mode: `opencode acp` over stdio). */
|
|
132
132
|
const OpencodeConfigSchema = z.object({
|
|
133
|
-
/** Provider ID for the LLM backend
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
/** Provider ID for the LLM backend. `opencode` is present in both v1 and v2
|
|
134
|
+
* catalogs; a user-selected provider/model overrides it. */
|
|
135
|
+
providerID: z.string().default('opencode'),
|
|
136
|
+
/** Model ID for the LLM backend. `big-pickle` exists in both v1 and v2. */
|
|
137
|
+
modelID: z.string().default('big-pickle'),
|
|
137
138
|
/** Session mode (opencode agent name): 'build' (default) or 'plan'. */
|
|
138
139
|
mode: z.enum(['build', 'plan']).default('build'),
|
|
139
140
|
/** ACP sub-configuration. Structurally mirrors kimi: the card exposes
|
|
@@ -189,8 +190,33 @@ const KimiConfigSchema = z.object({
|
|
|
189
190
|
export const DEFAULT_DSH_HOST = 'http://127.0.0.1:3080';
|
|
190
191
|
/** DSH (DeepSeek Harness) Web Host connection config. */
|
|
191
192
|
const DshConfigSchema = z.object({
|
|
192
|
-
/** DSH Web Host base URL (
|
|
193
|
+
/** DSH Web Host base URL (local). */
|
|
193
194
|
host: z.string().url().default(DEFAULT_DSH_HOST),
|
|
195
|
+
/**
|
|
196
|
+
* 全局 DSH 未运行时是否由 lark-remote 后台拉起(detached,lark-remote
|
|
197
|
+
* 退出后继续运行)。默认 true;设为 false 时只连接已运行的 DSH。
|
|
198
|
+
*/
|
|
199
|
+
manageProcess: z.boolean().default(true),
|
|
200
|
+
/** DSH 的 $DSH_HOME;默认 $DSH_HOME 或 ~/.dsh。用于启动锁和 DSH 日志目录。 */
|
|
201
|
+
dshHome: z.string().optional(),
|
|
202
|
+
/** DSH 可执行文件;默认 dsh。 */
|
|
203
|
+
dshCommand: z.string().optional(),
|
|
204
|
+
/** 后台拉起 DSH 后等待端口可用的上限(毫秒)。 */
|
|
205
|
+
startTimeoutMs: z.number().int().min(1000).max(600000).default(30_000),
|
|
206
|
+
/**
|
|
207
|
+
* DSH Web launch token (0.2.x requires auth). Paste the token from the URL
|
|
208
|
+
* printed by `dsh web`, or the whole URL — the client extracts `token`.
|
|
209
|
+
* Optional fallback: normally lark-remote mints cookies from the durable
|
|
210
|
+
* DSH browser-session secret instead.
|
|
211
|
+
*/
|
|
212
|
+
token: z.string().optional(),
|
|
213
|
+
/**
|
|
214
|
+
* DSH credentials file containing the durable browser-session signing
|
|
215
|
+
* secret. Defaults to `$DSH_HOME/.credentials.yaml` or
|
|
216
|
+
* `~/.dsh/.credentials.yaml`. Reading that secret lets lark-remote mint a
|
|
217
|
+
* fresh browser cookie after an external `dsh web` restart.
|
|
218
|
+
*/
|
|
219
|
+
credentialsFile: z.string().optional(),
|
|
194
220
|
/**
|
|
195
221
|
* Session preset(agentPreset)。preset 在 session 创建时固定,中途切换会被
|
|
196
222
|
* 服务端拒绝(agent-preset-conflict)。留空 = 跟随服务端默认(不传 agentPreset)。
|
|
@@ -198,10 +224,24 @@ const DshConfigSchema = z.object({
|
|
|
198
224
|
* 控制 + 运行时 DSH API 报错兜底。
|
|
199
225
|
*/
|
|
200
226
|
agentPreset: z.string().optional(),
|
|
227
|
+
/**
|
|
228
|
+
* 权限预设(沙箱模式 + 审批策略)。与 agentPreset 不同,它可以在会话中
|
|
229
|
+
* 切换;lark-remote 会在会话首次 run 前执行一次 `/permission <preset>`
|
|
230
|
+
* 对齐。留空 = 不干预,跟随 DSH 当前会话/服务端默认。
|
|
231
|
+
*/
|
|
232
|
+
permissionPreset: z.string().optional(),
|
|
233
|
+
/**
|
|
234
|
+
* 模型服务商(DSH provider route)。取值来自 `session/modelCatalog` 的
|
|
235
|
+
* `groups[].id`,例如 deepseek-official / deepseek-account / anthropic。
|
|
236
|
+
* 留空 = 兼容旧配置:按 deepseek-official 处理(与 provider 硬编码时代的
|
|
237
|
+
* 行为一致)。枚举不在此硬编码:provider 清单随 DSH profile bundle 与
|
|
238
|
+
* 用户配置变化。
|
|
239
|
+
*/
|
|
240
|
+
provider: z.string().optional(),
|
|
201
241
|
/** 模型 ID。留空 = 跟随服务端默认。 */
|
|
202
242
|
model: z.string().optional(),
|
|
203
243
|
/**
|
|
204
|
-
* 推理强度(off / low / high / max,跟随
|
|
244
|
+
* 推理强度(off / low / high / max,跟随 session/modelCatalog 实际档位)。留空 = 跟随
|
|
205
245
|
* 服务端默认。枚举不硬编码(理由同上 agentPreset)。
|
|
206
246
|
*/
|
|
207
247
|
reasoningEffort: z.string().optional(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode 配置读取工具
|
|
3
3
|
*
|
|
4
|
-
* 从 opencode models
|
|
4
|
+
* 从 opencode models 命令获取 provider 和 model 信息
|
|
5
5
|
*/
|
|
6
6
|
interface OpencodeConfigResult {
|
|
7
7
|
/** provider 名称列表 */
|
|
@@ -12,9 +12,9 @@ interface OpencodeConfigResult {
|
|
|
12
12
|
/** 清空缓存(仅供测试使用,生产代码无调用点) */
|
|
13
13
|
export declare function invalidateOpencodeConfigCache(): void;
|
|
14
14
|
/**
|
|
15
|
-
* 加载 opencode 配置,执行 opencode models
|
|
15
|
+
* 加载 opencode 配置,执行 opencode models 并解析
|
|
16
16
|
*
|
|
17
17
|
* @returns provider 列表和 model 查询接口
|
|
18
18
|
*/
|
|
19
|
-
export declare function loadOpencodeConfig(): OpencodeConfigResult;
|
|
19
|
+
export declare function loadOpencodeConfig(binary?: string): OpencodeConfigResult;
|
|
20
20
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode 配置读取工具
|
|
3
3
|
*
|
|
4
|
-
* 从 opencode models
|
|
4
|
+
* 从 opencode models 命令获取 provider 和 model 信息
|
|
5
5
|
*/
|
|
6
6
|
import { resolveExecutable } from '../platform/command.js';
|
|
7
7
|
import { spawnProcessSync } from '../platform/spawn.js';
|
|
@@ -13,7 +13,8 @@ import { getLogger } from '../logger/index.js';
|
|
|
13
13
|
*/
|
|
14
14
|
const FALLBACK_MODELS = {
|
|
15
15
|
opencode: ['big-pickle'],
|
|
16
|
-
|
|
16
|
+
'opencode-go': ['deepseek-v4.1-flash'],
|
|
17
|
+
deepseek: ['deepseek-chat', 'deepseek-flash', 'deepseek-v4-pro'],
|
|
17
18
|
'minimax-cn-coding-plan': ['MiniMax-M2.5'],
|
|
18
19
|
};
|
|
19
20
|
/** fallback provider 列表从模型表派生,保证不会出现空模型列表的 provider(历史曾混入无模型的 myprovider/volcengine-plan) */
|
|
@@ -51,33 +52,41 @@ function buildFallbackResult() {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
|
-
* 加载 opencode 配置,执行 opencode models
|
|
55
|
+
* 加载 opencode 配置,执行 opencode models 并解析
|
|
55
56
|
*
|
|
56
57
|
* @returns provider 列表和 model 查询接口
|
|
57
58
|
*/
|
|
58
|
-
export function loadOpencodeConfig() {
|
|
59
|
+
export function loadOpencodeConfig(binary = 'opencode') {
|
|
59
60
|
const now = Date.now();
|
|
60
61
|
// 正缓存命中:TTL 内不重复 execSync
|
|
61
|
-
if (opencodeConfigCache
|
|
62
|
+
if (opencodeConfigCache?.binary === binary &&
|
|
63
|
+
now - opencodeConfigCache.timestamp < OPENCODE_CONFIG_CACHE_TTL_MS) {
|
|
62
64
|
return opencodeConfigCache.result;
|
|
63
65
|
}
|
|
64
66
|
// 失败负缓存:命令最近失败过,TTL 内直接用 fallback,不再同步阻塞
|
|
65
|
-
if (opencodeConfigFailedAt
|
|
66
|
-
now - opencodeConfigFailedAt < OPENCODE_NEGATIVE_CACHE_TTL_MS) {
|
|
67
|
+
if (opencodeConfigFailedAt?.binary === binary &&
|
|
68
|
+
now - opencodeConfigFailedAt.timestamp < OPENCODE_NEGATIVE_CACHE_TTL_MS) {
|
|
67
69
|
return buildFallbackResult();
|
|
68
70
|
}
|
|
69
71
|
try {
|
|
70
72
|
// 经 seam 解析真实可执行文件(win32 PATHEXT),cross-spawn 同步执行
|
|
71
|
-
const resolved = resolveExecutable(
|
|
73
|
+
const resolved = resolveExecutable(binary);
|
|
72
74
|
if (!resolved)
|
|
73
75
|
throw new Error('opencode not found');
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
+
// v2 removed `--verbose`; plain `models` prints one `provider/model` per
|
|
77
|
+
// line in both v1 and v2. Try the warm shared daemon first (fast); if it is
|
|
78
|
+
// unavailable, retry with v2's private `--standalone` server.
|
|
79
|
+
const modelOpts = {
|
|
76
80
|
encoding: 'utf-8',
|
|
77
81
|
// 30s → 10s,与 codex/kimi 对齐;maxBuffer 防模型清单 ENOBUFS 隐性截断
|
|
78
82
|
timeout: 10000,
|
|
79
83
|
maxBuffer: 64 * 1024 * 1024,
|
|
80
|
-
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
};
|
|
86
|
+
let res = spawnProcessSync(resolved.file, ['models'], modelOpts);
|
|
87
|
+
if (res.error || res.status !== 0) {
|
|
88
|
+
res = spawnProcessSync(resolved.file, ['models', '--standalone'], modelOpts);
|
|
89
|
+
}
|
|
81
90
|
if (res.error)
|
|
82
91
|
throw res.error;
|
|
83
92
|
if (res.status !== 0) {
|
|
@@ -89,16 +98,23 @@ export function loadOpencodeConfig() {
|
|
|
89
98
|
// 解析输出:每个模型以 provider/model 格式开头,后面跟着 JSON 块
|
|
90
99
|
const lines = output.split('\n');
|
|
91
100
|
for (const line of lines) {
|
|
92
|
-
// 检测模型头如 "opencode/big-pickle" 或 "deepseek/deepseek-chat"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// 检测模型头如 "opencode/big-pickle" 或 "deepseek/deepseek-chat".
|
|
102
|
+
// Split on the FIRST slash: some providers expose model IDs that
|
|
103
|
+
// themselves contain "/" (e.g. openrouter/anthropic/...).
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
const slash = trimmed.indexOf('/');
|
|
106
|
+
if (slash <= 0 || slash === trimmed.length - 1)
|
|
107
|
+
continue;
|
|
108
|
+
const providerID = trimmed.slice(0, slash);
|
|
109
|
+
const modelID = trimmed.slice(slash + 1);
|
|
110
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(providerID))
|
|
111
|
+
continue;
|
|
112
|
+
if (/\s/.test(modelID))
|
|
113
|
+
continue;
|
|
114
|
+
if (!providerModels.has(providerID)) {
|
|
115
|
+
providerModels.set(providerID, []);
|
|
101
116
|
}
|
|
117
|
+
providerModels.get(providerID).push(modelID);
|
|
102
118
|
}
|
|
103
119
|
const providerNames = Array.from(providerModels.keys()).sort();
|
|
104
120
|
const modelOptions = (provider) => {
|
|
@@ -115,7 +131,7 @@ export function loadOpencodeConfig() {
|
|
|
115
131
|
return providerModels.get(provider) ?? [];
|
|
116
132
|
};
|
|
117
133
|
const result = { providerNames, modelOptions };
|
|
118
|
-
opencodeConfigCache = { result, timestamp: now };
|
|
134
|
+
opencodeConfigCache = { binary, result, timestamp: now };
|
|
119
135
|
opencodeConfigFailedAt = null;
|
|
120
136
|
return result;
|
|
121
137
|
}
|
|
@@ -123,7 +139,7 @@ export function loadOpencodeConfig() {
|
|
|
123
139
|
// 如果命令失败,使用 fallback
|
|
124
140
|
const logger = getLogger();
|
|
125
141
|
logger.warn(`[opencode-config] failed to load opencode models: ${error}`);
|
|
126
|
-
opencodeConfigFailedAt = now;
|
|
142
|
+
opencodeConfigFailedAt = { binary, timestamp: now };
|
|
127
143
|
return buildFallbackResult();
|
|
128
144
|
}
|
|
129
145
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboundAuthorizer:入站消息/媒体的统一 owner 授权入口。
|
|
3
|
+
*
|
|
4
|
+
* owner 绑定只发生在文本消息的 `binder.authorize()`
|
|
5
|
+
* 里,媒体入口只做只读 `isOwner()` 检查。新安装用户的第一条消息是纯图片/文件
|
|
6
|
+
* 时,媒体处理被静默丢弃、绑定也永远不完成。文本与媒体现在都走这里:
|
|
7
|
+
*
|
|
8
|
+
* - `rejected`:已绑定但不是 owner → 调用方静默丢弃;
|
|
9
|
+
* - `bind_success`:首条入站消息完成绑定 → 触发一次 onboarding(默认 cwd、
|
|
10
|
+
* 欢迎语、帮助卡),媒体调用方继续下载(纯媒体最终走 no-text 回执);
|
|
11
|
+
* - `owner`:正常放行。
|
|
12
|
+
*
|
|
13
|
+
* onboarding 用一次性 promise 去重:一条 post 同时带图和文字时,connector 会
|
|
14
|
+
* 先后触发媒体与文本两个入口,两个入口都看到 bind_success 也只跑一次。
|
|
15
|
+
*/
|
|
16
|
+
import type { OwnerBinder } from '../binder.js';
|
|
17
|
+
export type InboundAuthorization = 'owner' | 'bind_success' | 'rejected';
|
|
18
|
+
export interface InboundAuthorizerOptions {
|
|
19
|
+
binder: OwnerBinder;
|
|
20
|
+
/**
|
|
21
|
+
* 首次绑定时的副作用(同步设置默认 cwd + 异步发欢迎语/帮助卡)。
|
|
22
|
+
* 同步部分必须在返回前完成:用户的下一条消息要能找到已写好的 cwd。
|
|
23
|
+
*/
|
|
24
|
+
onBind: (userId: string, chatId: string) => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare class InboundAuthorizer {
|
|
27
|
+
private readonly opts;
|
|
28
|
+
private onboarding;
|
|
29
|
+
constructor(opts: InboundAuthorizerOptions);
|
|
30
|
+
/** 绑定/授权判定 + 首次绑定时触发 onboarding。 */
|
|
31
|
+
authorize(userId: string, chatId: string): InboundAuthorization;
|
|
32
|
+
/** 等待首次绑定的 onboarding 完成(测试 / 收尾用;生产路径不阻塞)。 */
|
|
33
|
+
whenBound(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getLogger } from '../logger/index.js';
|
|
2
|
+
export class InboundAuthorizer {
|
|
3
|
+
opts;
|
|
4
|
+
onboarding;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
/** 绑定/授权判定 + 首次绑定时触发 onboarding。 */
|
|
9
|
+
authorize(userId, chatId) {
|
|
10
|
+
const decision = this.opts.binder.authorize(userId, chatId);
|
|
11
|
+
if (decision.kind === 'bind_success' && this.onboarding === undefined) {
|
|
12
|
+
// onBind 同步调用(默认 cwd 的写入必须在返回前生效,下一条消息才会命中
|
|
13
|
+
// 已设置的 cwd);只把它的异步后续(发欢迎语/帮助卡)挂到 promise 上。
|
|
14
|
+
try {
|
|
15
|
+
this.onboarding = Promise.resolve(this.opts.onBind(userId, chatId)).catch((err) => {
|
|
16
|
+
getLogger().error('[binder] onboarding failed:', err);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
this.onboarding = Promise.resolve();
|
|
21
|
+
getLogger().error('[binder] onboarding failed:', err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return decision.kind;
|
|
25
|
+
}
|
|
26
|
+
/** 等待首次绑定的 onboarding 完成(测试 / 收尾用;生产路径不阻塞)。 */
|
|
27
|
+
whenBound() {
|
|
28
|
+
return this.onboarding ?? Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=authorize.js.map
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { warmCodexCatalogCache } from './config/codex-config.js';
|
|
|
10
10
|
import { SessionReaderRegistry, SessionStore } from './session/index.js';
|
|
11
11
|
import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
|
|
12
12
|
import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, COMPACT_ACTION_CMDS, } from './router/index.js';
|
|
13
|
+
import { DshDaemon } from './runner/dsh/daemon.js';
|
|
13
14
|
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
14
15
|
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
15
16
|
import { actionFeedbackText } from './router/card-action-feedback.js';
|
|
@@ -26,6 +27,7 @@ import { checkLatestVersion, compareVersions, runInstallLatest, formatUpdateHint
|
|
|
26
27
|
import { classifyRejection } from './error-classification.js';
|
|
27
28
|
import { WorkspaceStore } from './workspace/index.js';
|
|
28
29
|
import { InboundTurnAssembler } from './inbound/turn-assembler.js';
|
|
30
|
+
import { InboundAuthorizer } from './inbound/authorize.js';
|
|
29
31
|
import { stripPlaceholders } from './inbound/placeholder.js';
|
|
30
32
|
import { buildSessionHistoryCard } from './router/card-helpers.js';
|
|
31
33
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
@@ -136,6 +138,15 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
136
138
|
// 单点化:runner factory 取最新 config 的唯一入口(语义保持,
|
|
137
139
|
// container 未接线时回退启动快照)。
|
|
138
140
|
const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
|
|
141
|
+
// One global DSH per machine/DSH_HOME. This resolver reuses a listening
|
|
142
|
+
// process or starts `dsh web` detached; it never owns the child's lifetime.
|
|
143
|
+
const dshDaemon = new DshDaemon({
|
|
144
|
+
host: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
145
|
+
dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
|
|
146
|
+
dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
|
|
147
|
+
manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
|
|
148
|
+
startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
|
|
149
|
+
});
|
|
139
150
|
// Claude factory reads from configContainer (not closure) so runtime
|
|
140
151
|
// config changes (model, effort, stopGraceMs) take effect after
|
|
141
152
|
// bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
|
|
@@ -181,8 +192,14 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
181
192
|
sessionReaderRegistry.register('claude', new ClaudeSessionReader());
|
|
182
193
|
sessionReaderRegistry.register('codex', codexSessionReader);
|
|
183
194
|
// Register OpencodeSessionReader (CLI version)
|
|
184
|
-
// Using `opencode session list` and `
|
|
185
|
-
|
|
195
|
+
// Using `opencode session list` and `session export` instead of HTTP API.
|
|
196
|
+
// Share the configured binary with the ACP runner so `opencode2` (v2) can
|
|
197
|
+
// be used without the reader silently falling back to a v1 `opencode`.
|
|
198
|
+
// The reader is registered once; changing acp.binary at runtime requires a
|
|
199
|
+
// lark-remote restart.
|
|
200
|
+
const opencodeSessionReader = new OpencodeSessionReader({
|
|
201
|
+
binary: getAgentConfig(config, 'opencode')?.acp?.binary,
|
|
202
|
+
});
|
|
186
203
|
agentRegistry.register('opencode', (_ws) => {
|
|
187
204
|
const ocConfig = getAgentConfig(latest(), 'opencode');
|
|
188
205
|
// Build model string with validation: provider/model format
|
|
@@ -258,19 +275,39 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
258
275
|
// hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
|
|
259
276
|
// 新 host,reader(/resume、用量、完成卡)也跟随新 host。
|
|
260
277
|
hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
278
|
+
dshHomeProvider: () => getAgentConfig(latest(), 'dsh')?.dshHome,
|
|
279
|
+
tokenProvider: () => getAgentConfig(latest(), 'dsh')?.token,
|
|
280
|
+
credentialsFileProvider: () => getAgentConfig(latest(), 'dsh')?.credentialsFile,
|
|
261
281
|
});
|
|
262
282
|
agentRegistry.register('dsh', (_ws) => {
|
|
263
283
|
const dshConf = getAgentConfig(latest(), 'dsh');
|
|
284
|
+
// Warm the shared process in the background so the first message and
|
|
285
|
+
// `/resume` do not pay the full startup delay. Failures surface from
|
|
286
|
+
// DshRunner.run(), which awaits the same resolver.
|
|
287
|
+
void dshDaemon.ensure().catch((error) => {
|
|
288
|
+
getLogger().warn(`[dsh] background start failed: ${error.message}`);
|
|
289
|
+
});
|
|
264
290
|
return new DshRunner({
|
|
265
291
|
kind: 'dsh',
|
|
266
292
|
sessionReader: dshSessionReader,
|
|
293
|
+
daemon: dshDaemon,
|
|
267
294
|
host: dshConf?.host,
|
|
295
|
+
dshHome: dshConf?.dshHome,
|
|
296
|
+
token: dshConf?.token,
|
|
297
|
+
credentialsFile: dshConf?.credentialsFile,
|
|
268
298
|
agentPreset: dshConf?.agentPreset,
|
|
299
|
+
permissionPreset: dshConf?.permissionPreset,
|
|
300
|
+
provider: dshConf?.provider,
|
|
269
301
|
model: dshConf?.model,
|
|
270
302
|
reasoningEffort: dshConf?.reasoningEffort,
|
|
271
303
|
});
|
|
272
304
|
});
|
|
273
305
|
sessionReaderRegistry.register('dsh', dshSessionReader);
|
|
306
|
+
if (config.defaultAgent === 'dsh') {
|
|
307
|
+
void dshDaemon.ensure().catch((error) => {
|
|
308
|
+
getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
274
311
|
// 2026-07-17: 注册所有 agent 的显示名,实现单点真相
|
|
275
312
|
agentRegistry.registerDisplayName('claude', 'Claude');
|
|
276
313
|
agentRegistry.registerDisplayName('codex', 'Codex');
|
|
@@ -347,13 +384,69 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
347
384
|
},
|
|
348
385
|
});
|
|
349
386
|
router.setInboundFlusher(() => assembler.flushAll('flush'));
|
|
387
|
+
// 统一的入站授权入口(文本 + 媒体共用):未绑定时,首条入站消息——无论
|
|
388
|
+
// 纯文本、纯媒体还是图文混合——都完成 owner 绑定并触发一次 onboarding。
|
|
389
|
+
// 媒体入口原先只做只读 isOwner 检查,新安装用户第一条消息是截图时既不绑定、
|
|
390
|
+
// 也不保存、也没有任何反馈。
|
|
391
|
+
const authorizer = new InboundAuthorizer({
|
|
392
|
+
binder,
|
|
393
|
+
onBind: (userId, chatId) => {
|
|
394
|
+
// First-run onboarding: set default cwd + send welcome + Help card.
|
|
395
|
+
// setCwd is synchronous (before returning the async send promise) so the
|
|
396
|
+
// user's next message — which will hit the `owner` branch now that
|
|
397
|
+
// startup-contact.json has been written — finds cwd already set.
|
|
398
|
+
// process.cwd() is the only sensible default: the user started
|
|
399
|
+
// lark-remote in the directory they want to work in.
|
|
400
|
+
// setCwd only when no cwd is set yet (e.g. fresh start after bind).
|
|
401
|
+
// Don't overwrite an existing cwd — the user may have already set one
|
|
402
|
+
// via /cd or a previous bind. Use realpath for canonical form (matching
|
|
403
|
+
// Claude JSONL cwd, e.g. /tmp → /private/tmp on macOS).
|
|
404
|
+
let resolvedCwd;
|
|
405
|
+
try {
|
|
406
|
+
resolvedCwd = fs.realpathSync(process.cwd());
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
resolvedCwd = process.cwd();
|
|
410
|
+
}
|
|
411
|
+
if (!sessionStore.getCwd(userId)) {
|
|
412
|
+
sessionStore.setCwd(userId, resolvedCwd);
|
|
413
|
+
}
|
|
414
|
+
return (async () => {
|
|
415
|
+
try {
|
|
416
|
+
// Message 1: bind confirmation + status (combined to avoid
|
|
417
|
+
// Feishu out-of-order delivery across three separate messages).
|
|
418
|
+
await connector.sendWithRetry(chatId, {
|
|
419
|
+
text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
|
|
420
|
+
`📂 当前工作目录: \`${resolvedCwd}\`\n` +
|
|
421
|
+
`🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
|
|
422
|
+
`💡 直接输入消息即可开始对话,或 /cd 切换目录`,
|
|
423
|
+
});
|
|
424
|
+
// Message 2: Help card.
|
|
425
|
+
// Use router.cmdHelp() + manual send (without replyTo) so the Help
|
|
426
|
+
// card appears as an independent message, not a reply to the PIN
|
|
427
|
+
// message. router.handle('/help') would set replyTo=msg.messageId
|
|
428
|
+
// (the PIN), causing the card to nest under the PIN in Feishu UI.
|
|
429
|
+
const helpResult = router.cmdHelp();
|
|
430
|
+
if (helpResult.card) {
|
|
431
|
+
await connector.sendWithRetry(chatId, { card: helpResult.card });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
logger.error('[binder] onboarding send failed:', err);
|
|
436
|
+
}
|
|
437
|
+
})();
|
|
438
|
+
},
|
|
439
|
+
});
|
|
350
440
|
// 入站媒体(图片/文件/视频/语音/表情):先认证后下载(修复)。
|
|
351
441
|
// connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
|
|
352
442
|
// 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
|
|
353
443
|
// 下载是异步的:立刻把在途 promise 交给装配器(`kind: 'media'`),
|
|
354
444
|
// 保证「先图后文」不会先提交一个只含文本的 turn。
|
|
355
445
|
connector.setInboundMediaDetectedHandler((msg) => {
|
|
356
|
-
|
|
446
|
+
// 与文本入口共用授权:首条纯媒体也在这里完成 owner 绑定(bind_success
|
|
447
|
+
// 继续走下载 + 装配,纯媒体最终由装配器发 no-text 回执)。
|
|
448
|
+
const authorization = authorizer.authorize(msg.userId, msg.chatId);
|
|
449
|
+
if (authorization === 'rejected') {
|
|
357
450
|
logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
|
|
358
451
|
return;
|
|
359
452
|
}
|
|
@@ -402,58 +495,16 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
402
495
|
});
|
|
403
496
|
});
|
|
404
497
|
connector.setMessageHandler((msg) => {
|
|
405
|
-
// 绑定/授权闸门:仅 owner 放行;非 owner
|
|
406
|
-
|
|
407
|
-
|
|
498
|
+
// 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时首条消息认领。
|
|
499
|
+
// 与媒体入口共用 InboundAuthorizer:bind_success 的 onboarding 只跑一次
|
|
500
|
+
// (同一条 post 同时带图和文字时两个入口都会看到 bind_success)。
|
|
501
|
+
const decision = authorizer.authorize(msg.userId, msg.chatId);
|
|
502
|
+
if (decision === 'rejected')
|
|
408
503
|
return;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// setCwd is synchronous (outside the async closure) so that the user's
|
|
412
|
-
// next message — which will hit the `owner` branch now that
|
|
413
|
-
// startup-contact.json has been written — finds cwd already set.
|
|
414
|
-
// process.cwd() is the only sensible default: the user started
|
|
415
|
-
// lark-remote in the directory they want to work in.
|
|
416
|
-
// setCwd only when no cwd is set yet (e.g. fresh start after bind).
|
|
417
|
-
// Don't overwrite an existing cwd — the user may have already set one
|
|
418
|
-
// via /cd or a previous bind. Use realpath for canonical form (matching
|
|
419
|
-
// Claude JSONL cwd, e.g. /tmp → /private/tmp on macOS).
|
|
420
|
-
let resolvedCwd;
|
|
421
|
-
try {
|
|
422
|
-
resolvedCwd = fs.realpathSync(process.cwd());
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
resolvedCwd = process.cwd();
|
|
426
|
-
}
|
|
427
|
-
if (!sessionStore.getCwd(msg.userId)) {
|
|
428
|
-
sessionStore.setCwd(msg.userId, resolvedCwd);
|
|
429
|
-
}
|
|
430
|
-
void (async () => {
|
|
431
|
-
try {
|
|
432
|
-
// Message 1: bind confirmation + status (combined to avoid
|
|
433
|
-
// Feishu out-of-order delivery across three separate messages).
|
|
434
|
-
await connector.sendWithRetry(msg.chatId, {
|
|
435
|
-
text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
|
|
436
|
-
`📂 当前工作目录: \`${resolvedCwd}\`\n` +
|
|
437
|
-
`🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
|
|
438
|
-
`💡 直接输入消息即可开始对话,或 /cd 切换目录`,
|
|
439
|
-
});
|
|
440
|
-
// Message 2: Help card.
|
|
441
|
-
// Use router.cmdHelp() + manual send (without replyTo) so the Help
|
|
442
|
-
// card appears as an independent message, not a reply to the PIN
|
|
443
|
-
// message. router.handle('/help') would set replyTo=msg.messageId
|
|
444
|
-
// (the PIN), causing the card to nest under the PIN in Feishu UI.
|
|
445
|
-
const helpResult = router.cmdHelp();
|
|
446
|
-
if (helpResult.card) {
|
|
447
|
-
await connector.sendWithRetry(msg.chatId, { card: helpResult.card });
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
catch (err) {
|
|
451
|
-
logger.error('[binder] onboarding send failed:', err);
|
|
452
|
-
}
|
|
453
|
-
})();
|
|
504
|
+
// 首条消息只用于完成绑定,不转发给 agent(与旧行为一致)。
|
|
505
|
+
if (decision === 'bind_success')
|
|
454
506
|
return;
|
|
455
|
-
|
|
456
|
-
// decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
|
|
507
|
+
// decision === 'owner':正常处理(不再每条覆盖 startup-contact)
|
|
457
508
|
logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
|
|
458
509
|
// Add Typing reaction to indicate "still alive"
|
|
459
510
|
void connector.addReaction(msg.messageId, 'Typing');
|
|
@@ -497,17 +548,19 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
497
548
|
`(msg_type=${msg.rawContentType})`);
|
|
498
549
|
}
|
|
499
550
|
const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
|
|
500
|
-
//
|
|
501
|
-
//
|
|
551
|
+
// 命令消息旁路装配窗口:/命令 立即执行,但必须先等窗口内已装配的内容
|
|
552
|
+
// (含在途媒体下载)提交完成。此前 `void flush()` 不等待,
|
|
553
|
+
// 有下载在途时命令会先于 commit 执行——发送在 /cd、/new 前的消息被绑定到
|
|
554
|
+
// 新 cwd/session,媒体也落到新目录。/stop 已在上面走快速路径,不受影响。
|
|
502
555
|
if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
|
|
503
|
-
void
|
|
504
|
-
|
|
505
|
-
.handle(stripped.clean, {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
556
|
+
void (async () => {
|
|
557
|
+
await assembler.flush(msg.userId, msg.chatId, 'flush');
|
|
558
|
+
await router.handle(stripped.clean, {
|
|
559
|
+
userId: msg.userId,
|
|
560
|
+
chatId: msg.chatId,
|
|
561
|
+
messageId: msg.messageId,
|
|
562
|
+
}, { allowCommandPrefix: true });
|
|
563
|
+
})().catch((err) => logger.error('[control] command failed:', err));
|
|
511
564
|
return;
|
|
512
565
|
}
|
|
513
566
|
// 普通消息:进装配器,等静默期窗口到期(下载落定)后合并成一个 turn。
|
package/dist/instance-lock.d.ts
CHANGED
|
@@ -33,6 +33,15 @@ export declare class InstanceLock {
|
|
|
33
33
|
* 与 CIM CommandLine 首 token 归一后必然一致(bun/node 垫片场景亦然)。
|
|
34
34
|
*/
|
|
35
35
|
private lockContents;
|
|
36
|
+
/** 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。 */
|
|
37
|
+
private tryCreate;
|
|
38
|
+
/**
|
|
39
|
+
* 把夺来的活锁放回原位。优先 `link`(目标已存在会失败,不会覆盖更新的锁),
|
|
40
|
+
* 再删 quarantine。目标已被更新锁占据时放弃恢复——此时当前锁文件属于一个
|
|
41
|
+
* 活实例,拒绝取锁仍然是对的。
|
|
42
|
+
*/
|
|
43
|
+
private restoreQuarantine;
|
|
44
|
+
/** 读取锁文件内容(无副作用:不再在读取失败时 unlink 当前锁)。 */
|
|
36
45
|
private readPidAndName;
|
|
37
46
|
/**
|
|
38
47
|
* 锁是否仍由「我们的」实例持有(fail-open 三态):
|