lark-remote 0.1.12 → 0.2.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.
- package/CHANGELOG.md +37 -0
- package/README.en.md +2 -6
- package/README.md +3 -7
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -7
- package/dist/bridge/index.js +100 -133
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +14 -5
- package/dist/card/card-shared.js +23 -34
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.js +14 -11
- package/dist/config/index.d.ts +0 -8
- package/dist/config/index.js +0 -16
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +2 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +16 -10
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -3
- package/dist/index.js +14 -41
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +73 -6
- package/dist/router/index.js +663 -590
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -4
- package/dist/runner/claude/session.js +18 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
- package/dist/runner/common/acp/protocol-helpers.js +82 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +63 -53
- package/dist/runner/kimi/acp/runner.js +189 -334
- package/dist/runner/kimi/acp/translator.d.ts +18 -101
- package/dist/runner/kimi/acp/translator.js +61 -255
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +74 -297
- package/dist/runner/opencode/acp/translator.d.ts +28 -104
- package/dist/runner/opencode/acp/translator.js +54 -249
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +37 -64
- package/dist/session/codex/rollout-reader.d.ts +10 -14
- package/dist/session/codex/rollout-reader.js +126 -48
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +15 -11
- package/dist/session/kimi/sessions.d.ts +24 -4
- package/dist/session/kimi/sessions.js +99 -103
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +35 -15
- package/dist/session/pi/sessions.js +64 -95
- package/dist/update/index.d.ts +2 -3
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -7
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
package/dist/config/wizard.js
CHANGED
|
@@ -1,41 +1,83 @@
|
|
|
1
1
|
import { registerApp } from '@larksuite/channel';
|
|
2
|
-
import qrcode from 'qrcode-terminal';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import YAML from 'yaml';
|
|
6
5
|
import { AppConfigSchema } from './index.js';
|
|
6
|
+
import { needsQrImage, renderQrImage, renderTerminalQr } from './qr.js';
|
|
7
|
+
import { silentlyUnlink } from '../common/fs.js';
|
|
8
|
+
/** Where the fallback QR image is written (next to the config it will seed). */
|
|
9
|
+
const QR_IMAGE_NAME = 'qr-code.gif';
|
|
10
|
+
/**
|
|
11
|
+
* Print the scan-to-create QR code.
|
|
12
|
+
*
|
|
13
|
+
* The terminal symbol uses full blocks on Windows (half blocks there often
|
|
14
|
+
* render wrong and cannot be scanned); the image file is offered whenever the
|
|
15
|
+
* terminal cannot show a trustworthy symbol — Windows, or a window too narrow
|
|
16
|
+
* for the symbol to fit without wrapping.
|
|
17
|
+
*
|
|
18
|
+
* Returns true when the fallback image was written, so the caller only cleans
|
|
19
|
+
* up a file that actually exists.
|
|
20
|
+
*/
|
|
21
|
+
function printRegistrationQr(info, imagePath) {
|
|
22
|
+
console.log('请用飞书 App 扫描以下二维码完成应用创建:\n');
|
|
23
|
+
const qr = renderTerminalQr(info.url, { columns: process.stdout.columns });
|
|
24
|
+
if (qr)
|
|
25
|
+
console.log(`${qr.text}\n`);
|
|
26
|
+
else
|
|
27
|
+
console.log('(当前终端太窄,二维码显示不下:可拉宽窗口重试,或扫下面的图片)\n');
|
|
28
|
+
let wroteImage = false;
|
|
29
|
+
if (needsQrImage(qr)) {
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(imagePath, renderQrImage(info.url));
|
|
33
|
+
wroteImage = true;
|
|
34
|
+
console.log(`二维码图片:${imagePath}(或用图片查看器打开后扫码)`);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.log(`二维码图片写入失败:${String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const mins = Math.max(1, Math.round(info.expireIn / 60));
|
|
41
|
+
console.log(`\n二维码有效期:约 ${mins} 分钟`);
|
|
42
|
+
console.log(`也可以直接在浏览器打开:${info.url}\n`);
|
|
43
|
+
return wroteImage;
|
|
44
|
+
}
|
|
7
45
|
/**
|
|
8
46
|
* Run the QR-code registration wizard. The user scans the QR with their
|
|
9
47
|
* Feishu/Lark app to create a new application; the SDK polls until the
|
|
10
48
|
* creation completes and returns the credentials.
|
|
11
49
|
*/
|
|
12
|
-
async function runRegistrationWizard() {
|
|
50
|
+
async function runRegistrationWizard(configPath) {
|
|
13
51
|
console.log('\n[lark-remote] 未检测到飞书应用配置,进入扫码创建向导。\n');
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
const imagePath = path.join(path.dirname(configPath), QR_IMAGE_NAME);
|
|
53
|
+
let wroteImage = false;
|
|
54
|
+
try {
|
|
55
|
+
const result = await registerApp({
|
|
56
|
+
source: 'lark-remote',
|
|
57
|
+
onQRCodeReady: (info) => {
|
|
58
|
+
wroteImage = printRegistrationQr(info, imagePath);
|
|
59
|
+
},
|
|
60
|
+
onStatusChange: (info) => {
|
|
61
|
+
if (info.status === 'domain_switched') {
|
|
62
|
+
console.log('识别到国际版租户,已切换到 larksuite.com 域名。');
|
|
63
|
+
}
|
|
64
|
+
else if (info.status === 'slow_down') {
|
|
65
|
+
console.log('轮询速度过快,已自动降速。');
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
console.log('\n✓ 应用创建成功');
|
|
70
|
+
console.log(` App ID: ${result.client_id}`);
|
|
71
|
+
if (result.user_info?.tenant_brand) {
|
|
72
|
+
console.log(` Tenant: ${result.user_info.tenant_brand}`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
return { appId: result.client_id, appSecret: result.client_secret };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (wroteImage)
|
|
79
|
+
silentlyUnlink(imagePath);
|
|
36
80
|
}
|
|
37
|
-
console.log('');
|
|
38
|
-
return { appId: result.client_id, appSecret: result.client_secret };
|
|
39
81
|
}
|
|
40
82
|
function isInteractiveTerminal() {
|
|
41
83
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -77,7 +119,7 @@ export async function ensureConfig(configPath) {
|
|
|
77
119
|
return;
|
|
78
120
|
if (!isInteractiveTerminal())
|
|
79
121
|
return;
|
|
80
|
-
const { appId, appSecret } = await runRegistrationWizard();
|
|
122
|
+
const { appId, appSecret } = await runRegistrationWizard(configPath);
|
|
81
123
|
writeConfigWithCredentials(configPath, appId, appSecret);
|
|
82
124
|
console.log(`[lark-remote] 凭证已写入 ${configPath},继续启动...\n`);
|
|
83
125
|
}
|
|
@@ -6,9 +6,10 @@ import type { AppConfig } from '../config/index.js';
|
|
|
6
6
|
* 必须远小于用户连击间隔,否则同一按钮的连续点击会被当成"重复事件"丢弃:
|
|
7
7
|
* - SDK 的 cardAction dedup eventId = `card:{messageId}:{operator.openId}:{actionId}`
|
|
8
8
|
* - actionId = `tag|name|option|JSON.stringify(value)`,不含时间戳/事件序号
|
|
9
|
-
* - config 卡片原地更新(updateCardInPlace
|
|
9
|
+
* - config 卡片原地更新(updateCardInPlace),用户在同一张卡上重复触发
|
|
10
|
+
* 固定 value 的按钮(如 config.toggle):
|
|
10
11
|
* messageId / operator / value 三段都相同 → eventId 完全相同 → 第二次点击被
|
|
11
|
-
* seenCache drop,toggle
|
|
12
|
+
* seenCache drop,toggle 不可逆(第二次点击静默失效)。
|
|
12
13
|
*
|
|
13
14
|
* SDK 默认 12h 太长;60s 仍会误伤连击。300ms 挡飞书瞬时重投递(<100ms 级),
|
|
14
15
|
* 放过用户连击(慢点两次通常 >500ms)。代价:削弱 message 秒级重投递防护——
|
package/dist/connector/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { sleep } from '../common/sleep.js';
|
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import { silentlyUnlink } from '../common/fs.js';
|
|
9
|
+
import { displayName } from '../platform/path.js';
|
|
9
10
|
import https from 'node:https';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -29,9 +30,10 @@ const noKeepAliveAgent = new https.Agent({ keepAlive: false });
|
|
|
29
30
|
* 必须远小于用户连击间隔,否则同一按钮的连续点击会被当成"重复事件"丢弃:
|
|
30
31
|
* - SDK 的 cardAction dedup eventId = `card:{messageId}:{operator.openId}:{actionId}`
|
|
31
32
|
* - actionId = `tag|name|option|JSON.stringify(value)`,不含时间戳/事件序号
|
|
32
|
-
* - config 卡片原地更新(updateCardInPlace
|
|
33
|
+
* - config 卡片原地更新(updateCardInPlace),用户在同一张卡上重复触发
|
|
34
|
+
* 固定 value 的按钮(如 config.toggle):
|
|
33
35
|
* messageId / operator / value 三段都相同 → eventId 完全相同 → 第二次点击被
|
|
34
|
-
* seenCache drop,toggle
|
|
36
|
+
* seenCache drop,toggle 不可逆(第二次点击静默失效)。
|
|
35
37
|
*
|
|
36
38
|
* SDK 默认 12h 太长;60s 仍会误伤连击。300ms 挡飞书瞬时重投递(<100ms 级),
|
|
37
39
|
* 放过用户连击(慢点两次通常 >500ms)。代价:削弱 message 秒级重投递防护——
|
|
@@ -403,7 +405,7 @@ export class FeishuConnector {
|
|
|
403
405
|
if (stat.size > MAX_FILE_UPLOAD_SIZE) {
|
|
404
406
|
throw new Error(`File too large (${(stat.size / 1_000_000).toFixed(1)}MB), exceeds ${MAX_FILE_UPLOAD_SIZE / (1024 * 1024)}MB limit`);
|
|
405
407
|
}
|
|
406
|
-
const fileName = filePath
|
|
408
|
+
const fileName = displayName(filePath);
|
|
407
409
|
// P2-18: capture the read stream so it can be destroyed on any failure
|
|
408
410
|
// path. Without this, a token/upload/send failure leaks the fd (axios
|
|
409
411
|
// only closes the stream when it fully consumes it on success).
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { probeAllAgents } from './runner/probe.js';
|
|
|
9
9
|
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
|
-
import { CommandRouter, isImmediateAction } from './router/index.js';
|
|
12
|
+
import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, } from './router/index.js';
|
|
13
13
|
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
14
14
|
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
15
15
|
import { Bridge } from './bridge/index.js';
|
|
@@ -117,13 +117,14 @@ function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
|
|
|
117
117
|
/** Initialize per-workspace runner factory + agent registry. */
|
|
118
118
|
function initializeRunner(config, configDir, cliArgs) {
|
|
119
119
|
const agentRegistry = new AgentRegistry();
|
|
120
|
+
// W2.10 单点化:runner factory 取最新 config 的唯一入口(P1-15 语义保持,
|
|
121
|
+
// container 未接线时回退启动快照)。
|
|
122
|
+
const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
|
|
120
123
|
// P1-15: Claude factory reads from configContainer (not closure) so runtime
|
|
121
124
|
// config changes (model, effort, stopGraceMs) take effect after
|
|
122
125
|
// bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
|
|
123
126
|
agentRegistry.register('claude', (ws) => {
|
|
124
|
-
const
|
|
125
|
-
const latestConfig = container?.current ?? config;
|
|
126
|
-
const claudeConfig = latestConfig.claude;
|
|
127
|
+
const claudeConfig = latest().claude;
|
|
127
128
|
return new ClaudeRunner({
|
|
128
129
|
model: claudeConfig.model,
|
|
129
130
|
effort: claudeConfig.effort,
|
|
@@ -141,9 +142,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
141
142
|
// + clearRunners().
|
|
142
143
|
const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
143
144
|
agentRegistry.register('codex', (_ws) => {
|
|
144
|
-
const
|
|
145
|
-
const latestConfig = container?.current ?? config;
|
|
146
|
-
const codexConfig = getAgentConfig(latestConfig, 'codex');
|
|
145
|
+
const codexConfig = getAgentConfig(latest(), 'codex');
|
|
147
146
|
return new CodexAppServerRunner({
|
|
148
147
|
kind: 'codex',
|
|
149
148
|
model: codexConfig?.model,
|
|
@@ -169,10 +168,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
169
168
|
// Using `opencode session list` and `opencode export` instead of HTTP API
|
|
170
169
|
const opencodeSessionReader = new OpencodeSessionReader();
|
|
171
170
|
agentRegistry.register('opencode', (_ws) => {
|
|
172
|
-
|
|
173
|
-
const container = agentRegistry.getConfigContainer();
|
|
174
|
-
const latestConfig = container?.current ?? config;
|
|
175
|
-
const ocConfig = getAgentConfig(latestConfig, 'opencode');
|
|
171
|
+
const ocConfig = getAgentConfig(latest(), 'opencode');
|
|
176
172
|
// Build model string with validation: provider/model format
|
|
177
173
|
let model;
|
|
178
174
|
if (ocConfig?.modelID) {
|
|
@@ -201,10 +197,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
201
197
|
agentRegistry.setConfigContainer(configContainer);
|
|
202
198
|
const piSessionReader = new PiSessionReader();
|
|
203
199
|
agentRegistry.register('pi', (ws) => {
|
|
204
|
-
|
|
205
|
-
const container = agentRegistry.getConfigContainer();
|
|
206
|
-
const latestConfig = container?.current ?? config;
|
|
207
|
-
const piConf = getAgentConfig(latestConfig, 'pi');
|
|
200
|
+
const piConf = getAgentConfig(latest(), 'pi');
|
|
208
201
|
return new PiRpcRunner({
|
|
209
202
|
provider: piConf?.provider ?? 'Volcano',
|
|
210
203
|
model: piConf?.model ?? 'glm-5.2',
|
|
@@ -221,10 +214,7 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
221
214
|
// Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
|
|
222
215
|
const kimiSessionReader = new KimiSessionReader();
|
|
223
216
|
agentRegistry.register('kimi', (_ws) => {
|
|
224
|
-
|
|
225
|
-
const container = agentRegistry.getConfigContainer();
|
|
226
|
-
const latestConfig = container?.current ?? config;
|
|
227
|
-
const kimiConf = getAgentConfig(latestConfig, 'kimi');
|
|
217
|
+
const kimiConf = getAgentConfig(latest(), 'kimi');
|
|
228
218
|
const acpConf = kimiConf?.acp;
|
|
229
219
|
return new KimiAcpRunner({
|
|
230
220
|
kind: 'kimi',
|
|
@@ -244,18 +234,10 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
244
234
|
const dshSessionReader = new DshSessionReader({
|
|
245
235
|
// hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
|
|
246
236
|
// 新 host,reader(/resume、用量、完成卡)也跟随新 host(CC-02)。
|
|
247
|
-
hostProvider: () =>
|
|
248
|
-
const container = agentRegistry.getConfigContainer();
|
|
249
|
-
const latestConfig = container?.current ?? config;
|
|
250
|
-
return getAgentConfig(latestConfig, 'dsh')?.host;
|
|
251
|
-
},
|
|
237
|
+
hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
252
238
|
});
|
|
253
239
|
agentRegistry.register('dsh', (_ws) => {
|
|
254
|
-
|
|
255
|
-
// after bridge.setConfig() + clearRunners().
|
|
256
|
-
const container = agentRegistry.getConfigContainer();
|
|
257
|
-
const latestConfig = container?.current ?? config;
|
|
258
|
-
const dshConf = getAgentConfig(latestConfig, 'dsh');
|
|
240
|
+
const dshConf = getAgentConfig(latest(), 'dsh');
|
|
259
241
|
return new DshRunner({
|
|
260
242
|
kind: 'dsh',
|
|
261
243
|
sessionReader: dshSessionReader,
|
|
@@ -541,18 +523,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
541
523
|
// order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
|
|
542
524
|
// 需直返避免被吞(order.textInput 曾漏在列表外,编辑卡停留在编辑界面)。
|
|
543
525
|
// approval.planFeedback 同 answer 系列:附意见的 toast 需直返 + 不落串行队列。
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
actionValue.cmd === 'order.textInput' ||
|
|
548
|
-
actionValue.cmd === 'config.save' ||
|
|
549
|
-
actionValue.cmd === 'approval.respond' ||
|
|
550
|
-
actionValue.cmd === 'approval.toggle' ||
|
|
551
|
-
actionValue.cmd === 'approval.answer' ||
|
|
552
|
-
actionValue.cmd === 'approval.answerSubmit' ||
|
|
553
|
-
actionValue.cmd === 'approval.answerCustom' ||
|
|
554
|
-
actionValue.cmd === 'approval.answerNote' ||
|
|
555
|
-
actionValue.cmd === 'approval.planFeedback') {
|
|
526
|
+
// W2.1:直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
|
|
527
|
+
// order.textInput 曾漏在列表外、answer 家族曾漏在直返列表外——两份拷贝漂移)。
|
|
528
|
+
if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
|
|
556
529
|
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
557
530
|
}
|
|
558
531
|
// order.exec → equivalent queued message (Plan A): resolve the order text
|
package/dist/instance-lock.js
CHANGED
|
@@ -116,6 +116,13 @@ export class InstanceLock {
|
|
|
116
116
|
process.exit(129);
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
else {
|
|
120
|
+
// win32 无 SIGHUP;CTRL_BREAK 退出前也要释放锁(§3.5,对照 cli.ts 信号转发)
|
|
121
|
+
process.on('SIGBREAK', () => {
|
|
122
|
+
this.release();
|
|
123
|
+
process.exit(149);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
119
126
|
}
|
|
120
127
|
/**
|
|
121
128
|
* Lock file stores `pid\ncomm` where comm is the owning process's command
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atomically write content (string or binary Buffer) to a file via tmp + rename.
|
|
3
3
|
* Falls back to copy+unlink when rename fails with EXDEV (cross-device).
|
|
4
|
+
* rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试(§6);重试耗尽后
|
|
5
|
+
* 保留 tmp 现场并告警,再抛出原错误。
|
|
4
6
|
*/
|
|
5
7
|
export declare function atomicWrite(filePath: string, content: string | Buffer, encoding?: BufferEncoding): void;
|
|
6
8
|
/**
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { silentlyUnlink } from '../common/fs.js';
|
|
3
|
+
import { silentlyUnlink, withBusyRetry } from '../common/fs.js';
|
|
4
|
+
import { getLogger } from '../logger/index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Atomically write content (string or binary Buffer) to a file via tmp + rename.
|
|
6
7
|
* Falls back to copy+unlink when rename fails with EXDEV (cross-device).
|
|
8
|
+
* rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试(§6);重试耗尽后
|
|
9
|
+
* 保留 tmp 现场并告警,再抛出原错误。
|
|
7
10
|
*/
|
|
8
11
|
export function atomicWrite(filePath, content, encoding = 'utf-8') {
|
|
9
12
|
const dir = path.dirname(filePath);
|
|
@@ -33,8 +36,17 @@ export function atomicWrite(filePath, content, encoding = 'utf-8') {
|
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
let preserveTmpScene = false;
|
|
36
40
|
try {
|
|
37
|
-
fs.renameSync(tmpPath, filePath)
|
|
41
|
+
withBusyRetry(() => fs.renameSync(tmpPath, filePath), {
|
|
42
|
+
onGiveUp: (err) => {
|
|
43
|
+
// 保留现场:tmp 里是已写好的完整内容,重试耗尽(win32)或 posix 首次
|
|
44
|
+
// 占用即失败都说明目标不可达;留着 tmp 供人工恢复,只告警不静默
|
|
45
|
+
preserveTmpScene = true;
|
|
46
|
+
getLogger().warn(`[atomic-write] rename to ${filePath} still busy after retries; ` +
|
|
47
|
+
`tmp preserved at ${tmpPath}: ${String(err)}`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
38
50
|
}
|
|
39
51
|
catch (err) {
|
|
40
52
|
if (err.code === 'EXDEV') {
|
|
@@ -48,8 +60,11 @@ export function atomicWrite(filePath, content, encoding = 'utf-8') {
|
|
|
48
60
|
finally {
|
|
49
61
|
// Best-effort cleanup: on success the tmp was renamed away (ENOENT here,
|
|
50
62
|
// silently ignored); on EXDEV it was copied and now needs removal; on other
|
|
51
|
-
// errors we must not leak the tmp file
|
|
52
|
-
|
|
63
|
+
// errors we must not leak the tmp file — except when the scene is
|
|
64
|
+
// deliberately preserved after busy-retry exhaustion.
|
|
65
|
+
if (!preserveTmpScene) {
|
|
66
|
+
silentlyUnlink(tmpPath);
|
|
67
|
+
}
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
/**
|
|
@@ -66,7 +81,7 @@ export function atomicWriteJson(filePath, data) {
|
|
|
66
81
|
*/
|
|
67
82
|
export function atomicMoveFile(srcPath, destPath) {
|
|
68
83
|
try {
|
|
69
|
-
fs.renameSync(srcPath, destPath);
|
|
84
|
+
withBusyRetry(() => fs.renameSync(srcPath, destPath));
|
|
70
85
|
}
|
|
71
86
|
catch (err) {
|
|
72
87
|
if (err.code === 'EXDEV') {
|
|
@@ -4,6 +4,5 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @param filePath - Path to the JSON file
|
|
6
6
|
* @param fallback - Value to return if file doesn't exist or is corrupted
|
|
7
|
-
* @param parseFn - Optional custom parser (defaults to JSON.parse)
|
|
8
7
|
*/
|
|
9
|
-
export declare function loadJsonFile<T>(filePath: string, fallback: T
|
|
8
|
+
export declare function loadJsonFile<T>(filePath: string, fallback: T): T;
|
|
@@ -6,14 +6,13 @@ import { getLogger } from '../logger/index.js';
|
|
|
6
6
|
*
|
|
7
7
|
* @param filePath - Path to the JSON file
|
|
8
8
|
* @param fallback - Value to return if file doesn't exist or is corrupted
|
|
9
|
-
* @param parseFn - Optional custom parser (defaults to JSON.parse)
|
|
10
9
|
*/
|
|
11
|
-
export function loadJsonFile(filePath, fallback
|
|
10
|
+
export function loadJsonFile(filePath, fallback) {
|
|
12
11
|
try {
|
|
13
12
|
if (!fs.existsSync(filePath))
|
|
14
13
|
return fallback;
|
|
15
14
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
16
|
-
return
|
|
15
|
+
return JSON.parse(raw);
|
|
17
16
|
}
|
|
18
17
|
catch (err) {
|
|
19
18
|
getLogger().warn(`[persistence] failed to load ${filePath}, using fallback:`, err);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每 agent 的协议停止通道注册表(design.md §3.3)。
|
|
3
|
+
*
|
|
4
|
+
* Windows 没有可拦截的跨进程 SIGTERM,优雅停止只能靠协议通道(claude 的 stdin
|
|
5
|
+
* 控制通道、codex 的 turn interrupt、ACP 的 abort…)。「无通道」是**合法状态**:
|
|
6
|
+
* 由 Terminator 显式跳过优雅段并打日志,绝不允许悄悄等一个永远不会来的退出事件。
|
|
7
|
+
*
|
|
8
|
+
* 独立成文件是为了避免 `terminator.ts`(接口 + 分发)与 `terminator-win32.ts`
|
|
9
|
+
* 互相 import 形成循环依赖。
|
|
10
|
+
*/
|
|
11
|
+
import type { ChildProcess } from 'node:child_process';
|
|
12
|
+
export type AgentStopper = (proc: ChildProcess) => void | Promise<void>;
|
|
13
|
+
/** agent key → 协议停止通道。测试请自建实例以隔离。 */
|
|
14
|
+
export declare class AgentStopperRegistry {
|
|
15
|
+
private readonly stoppers;
|
|
16
|
+
register(agent: string, stopper: AgentStopper): void;
|
|
17
|
+
get(agent: string): AgentStopper | undefined;
|
|
18
|
+
has(agent: string): boolean;
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
21
|
+
/** 接线用的进程级单例;runner 在 run 启动时注册自己的停止通道。 */
|
|
22
|
+
export declare const agentStopperRegistry: AgentStopperRegistry;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** agent key → 协议停止通道。测试请自建实例以隔离。 */
|
|
2
|
+
export class AgentStopperRegistry {
|
|
3
|
+
stoppers = new Map();
|
|
4
|
+
register(agent, stopper) {
|
|
5
|
+
this.stoppers.set(agent, stopper);
|
|
6
|
+
}
|
|
7
|
+
get(agent) {
|
|
8
|
+
return this.stoppers.get(agent);
|
|
9
|
+
}
|
|
10
|
+
has(agent) {
|
|
11
|
+
return this.stoppers.has(agent);
|
|
12
|
+
}
|
|
13
|
+
clear() {
|
|
14
|
+
this.stoppers.clear();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** 接线用的进程级单例;runner 在 run 启动时注册自己的停止通道。 */
|
|
18
|
+
export const agentStopperRegistry = new AgentStopperRegistry();
|
|
19
|
+
//# sourceMappingURL=agent-stopper.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LaunchSpec } from './types.js';
|
|
2
|
+
export type { LaunchSpec } from './types.js';
|
|
3
|
+
export interface ResolveOptions {
|
|
4
|
+
platform?: NodeJS.Platform;
|
|
5
|
+
/** PATH 环境变量注入(测试用);默认 process.env.PATH */
|
|
6
|
+
pathEnv?: string;
|
|
7
|
+
/** PATHEXT 注入(测试用);win32 默认 .exe/.bat/.cmd(Windows 默认相对顺序) */
|
|
8
|
+
pathExt?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function clearResolveCache(): void;
|
|
11
|
+
export declare function resolveExecutable(name: string, opts?: ResolveOptions): LaunchSpec | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveExecutable —— 可执行文件发现。
|
|
3
|
+
*
|
|
4
|
+
* 职责收窄为「探测/诊断」:报告 agent 装在哪、装没装(可用性探测 probe.ts、
|
|
5
|
+
* 自更新安装源检测 update/install.ts)。**执行**统一走 platform/spawn.ts 的
|
|
6
|
+
* cross-spawn 包装(PATHEXT 解析 + .cmd 经 cmd.exe 受控执行),本模块不再
|
|
7
|
+
* 解析 .cmd/.bat 垫片内容——垫片格式是 npm/pnpm 的上游实现细节(历史变过、
|
|
8
|
+
* pnpm 生成形态不同),手写解析器等于耦合该细节(v2 §4.2)。
|
|
9
|
+
*
|
|
10
|
+
* posix:PATH 目录顺序 + X_OK(现状语义迁移)。
|
|
11
|
+
* win32:PATH 目录顺序 × PATHEXT(.exe/.cmd/.bat) 解析,.cmd 命中即报告垫片
|
|
12
|
+
* 路径本身(执行时由 cross-spawn 处理),结果按 (platform, pathEnv, name) 缓存。
|
|
13
|
+
*
|
|
14
|
+
* 分隔符/join 全部跟随注入 platform(win32 为 `;` + path.win32.join),
|
|
15
|
+
* 不读宿主 path——posix 宿主模拟 win32 的测试才保真。
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { currentPlatform, isWin32 } from './select.js';
|
|
20
|
+
const DEFAULT_PATHEXT = ['.exe', '.bat', '.cmd'];
|
|
21
|
+
/**
|
|
22
|
+
* 缓存 TTL:probe 层(runner/probe.ts)5 分钟,本层更短——bridge 运行期间
|
|
23
|
+
* 用户新装/卸载 agent(典型场景:按 /config 不可用提示去安装)后,最迟
|
|
24
|
+
* TTL 过期即可被重新探测到,不必重启。null(未找到)结果同样过期重查。
|
|
25
|
+
*/
|
|
26
|
+
const RESOLVE_CACHE_TTL_MS = 60_000;
|
|
27
|
+
const resolveCache = new Map();
|
|
28
|
+
export function clearResolveCache() {
|
|
29
|
+
resolveCache.clear();
|
|
30
|
+
}
|
|
31
|
+
/** 过期条目惰性清除;命中返回 spec,未命中/过期返回 undefined。 */
|
|
32
|
+
function cacheGet(key) {
|
|
33
|
+
const entry = resolveCache.get(key);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (Date.now() - entry.ts >= RESOLVE_CACHE_TTL_MS) {
|
|
37
|
+
resolveCache.delete(key);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entry.spec;
|
|
41
|
+
}
|
|
42
|
+
function listPathDirs(pathEnv, platform) {
|
|
43
|
+
const delimiter = isWin32(platform) ? ';' : ':';
|
|
44
|
+
return pathEnv.split(delimiter).filter((dir) => dir.length > 0);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* win32 join:真实 win32 宿主产出原生反斜杠路径;posix 宿主模拟 win32 的
|
|
48
|
+
* 测试里 dir 是 posix 风格路径,win32 join 会把分隔符归一成 `\` 而宿主 fs
|
|
49
|
+
* 读不到——回退正斜杠形态再探测一次(Windows fs 本身两种分隔符都认)。
|
|
50
|
+
*/
|
|
51
|
+
function existsWin32(file) {
|
|
52
|
+
if (fs.existsSync(file))
|
|
53
|
+
return file;
|
|
54
|
+
const posixForm = file.replaceAll('\\', '/');
|
|
55
|
+
if (posixForm !== file && fs.existsSync(posixForm))
|
|
56
|
+
return posixForm;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function resolveWin32(name, pathEnv, pathExt) {
|
|
60
|
+
const hasExtension = /\.[A-Za-z0-9]+$/.test(name);
|
|
61
|
+
for (const dir of listPathDirs(pathEnv, 'win32')) {
|
|
62
|
+
const candidates = hasExtension ? [name] : pathExt.map((ext) => name + ext);
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
const found = existsWin32(path.win32.join(dir, candidate));
|
|
65
|
+
// .cmd/.bat 垫片命中即报告垫片路径本身;执行语义由 cross-spawn 负责
|
|
66
|
+
if (found)
|
|
67
|
+
return { kind: 'direct', file: found };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function resolvePosix(name, pathEnv) {
|
|
73
|
+
for (const dir of listPathDirs(pathEnv, 'linux')) {
|
|
74
|
+
// 与 win32 分支对称:join 必须跟随注入 platform,而非宿主 path。
|
|
75
|
+
// 宿主是 win32 时用 path.join 会把 `/tmp/a` + `agent` 拼成 `\tmp\a\agent`,
|
|
76
|
+
// posix 语义测试在 Windows 宿主上必然落空。
|
|
77
|
+
const file = path.posix.join(dir, name);
|
|
78
|
+
try {
|
|
79
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
80
|
+
return { kind: 'direct', file };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// 不是可执行文件(或不存在)→ 尝试下一个 PATH 目录
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
export function resolveExecutable(name, opts) {
|
|
89
|
+
// 空名守卫:posix 的 path.join(dir, '') = 目录本身,而目录有 X_OK——
|
|
90
|
+
// 不挡住会把第一个 PATH 目录误报为「可执行」(dsh 空 binary 之类的假阳性)
|
|
91
|
+
const trimmed = name.trim();
|
|
92
|
+
if (!trimmed)
|
|
93
|
+
return null;
|
|
94
|
+
const platform = opts?.platform ?? currentPlatform;
|
|
95
|
+
const pathEnv = opts?.pathEnv ?? process.env.PATH ?? '';
|
|
96
|
+
const pathExt = opts?.pathExt ?? DEFAULT_PATHEXT;
|
|
97
|
+
const key = JSON.stringify([platform, pathEnv, [...pathExt].join(';'), trimmed]);
|
|
98
|
+
const cached = cacheGet(key);
|
|
99
|
+
if (cached !== undefined)
|
|
100
|
+
return cached;
|
|
101
|
+
const spec = isWin32(platform)
|
|
102
|
+
? resolveWin32(trimmed, pathEnv, pathExt)
|
|
103
|
+
: resolvePosix(trimmed, pathEnv);
|
|
104
|
+
resolveCache.set(key, { spec, ts: Date.now() });
|
|
105
|
+
return spec;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 进程身份快照。creationDate 仅 win32(CIM CreationDate,DMTF 时间串)。 */
|
|
2
|
+
export interface ProcessIdentity {
|
|
3
|
+
commandLine: string;
|
|
4
|
+
creationDate?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface QueryIdentityOptions {
|
|
7
|
+
/** 平台注入(测试用);默认当前宿主平台 */
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
/** 查询超时(毫秒),超时杀掉查询进程并返回 null */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface VerifyPidIdentityOptions extends QueryIdentityOptions {
|
|
13
|
+
/** 期望的二进制名(basename,如 'claude'/'node');win32/darwin 大小写不敏感 */
|
|
14
|
+
expectedBinary: string;
|
|
15
|
+
/** pid 文件记录的 CreationDate;旧文件缺省该字段则退化为仅命令行匹配 */
|
|
16
|
+
expectedCreationDate?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 查询进程身份;进程不存在或查询失败返回 null。
|
|
20
|
+
*/
|
|
21
|
+
export declare function queryProcessIdentity(pid: number, opts?: QueryIdentityOptions): Promise<ProcessIdentity | null>;
|
|
22
|
+
/**
|
|
23
|
+
* 引号感知的命令行 tokenizer:双引号段不按空白拆分。CIM 的 CommandLine 里
|
|
24
|
+
* "C:\Program Files\nodejs\node.exe" 这类含空格路径必须整段保留,朴素
|
|
25
|
+
* split(/\s+/) 会切碎 token 导致身份假阴性。
|
|
26
|
+
*/
|
|
27
|
+
export declare function tokenizeCommandLine(commandLine: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* pid 身份验证:进程存活且命令行确实属于期望二进制(+ CreationDate 一致)才 true。
|
|
30
|
+
*
|
|
31
|
+
* 用于 killOrphan 与实例锁陈旧判定——只有这里返回 true 才允许动手。
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyPidIdentity(pid: number, opts: VerifyPidIdentityOptions): Promise<boolean>;
|