lark-remote 0.1.6 → 0.1.7
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 +23 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +57 -34
- package/dist/bridge/approval-coordinator.js +191 -40
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +78 -7
- package/dist/bridge/index.js +202 -79
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +152 -36
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +1 -12
- package/dist/card/card-shared.js +1 -26
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +99 -232
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/index.d.ts +88 -23
- package/dist/config/index.js +105 -34
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +123 -105
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.js +29 -50
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +5 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +11 -2
- package/dist/router/index.d.ts +56 -21
- package/dist/router/index.js +373 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +4 -0
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
- package/dist/runner/codex/app-server/protocol-types.js +1 -12
- package/dist/runner/codex/app-server/runner.d.ts +42 -59
- package/dist/runner/codex/app-server/runner.js +160 -337
- package/dist/runner/codex/app-server/translator.d.ts +1 -21
- package/dist/runner/codex/app-server/translator.js +0 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/index.d.ts +4 -4
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +43 -13
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +14 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -66
- package/dist/runner/codex/runner.js +0 -129
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
package/dist/config/index.js
CHANGED
|
@@ -9,6 +9,20 @@ export { getConfigDir } from './dir.js';
|
|
|
9
9
|
import { KIMI_THINKING_EFFORTS } from './kimi-config.js';
|
|
10
10
|
/** Valid effort values for Claude */
|
|
11
11
|
export const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
12
|
+
/**
|
|
13
|
+
* Claude 官方 --permission-mode 枚举(claude --help 输出,2026-08 实测)。
|
|
14
|
+
* 额外支持 'default':Claude settings.json 的官方未设置值,等价于不传
|
|
15
|
+
* --permission-mode(交互式默认:高风险工具逐个询问)。
|
|
16
|
+
*/
|
|
17
|
+
export const CLAUDE_PERMISSION_MODES = [
|
|
18
|
+
'default',
|
|
19
|
+
'acceptEdits',
|
|
20
|
+
'auto',
|
|
21
|
+
'bypassPermissions',
|
|
22
|
+
'manual',
|
|
23
|
+
'dontAsk',
|
|
24
|
+
'plan',
|
|
25
|
+
];
|
|
12
26
|
/** Valid thinking levels for Pi */
|
|
13
27
|
export const PI_THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
14
28
|
/** 默认值常量:schema 与 template 共享,避免双源不一致(G25 Magic Numbers)。 */
|
|
@@ -17,6 +31,10 @@ const DEFAULTS = {
|
|
|
17
31
|
STOP_GRACE_MS: 5000,
|
|
18
32
|
IDLE_WATCHDOG_MINUTES: 15,
|
|
19
33
|
TURN_IDLE_TIMEOUT_MINUTES: 10,
|
|
34
|
+
/** 审批超时默认 5 分钟(对齐 codex 红线:勿随意改短)。 */
|
|
35
|
+
APPROVAL_TIMEOUT_MS: 5 * 60 * 1000,
|
|
36
|
+
/** Claude 会话级空闲回收默认 30 分钟(对齐 codex appServer.idleTtlMs)。 */
|
|
37
|
+
CLAUDE_IDLE_TTL_MINUTES: 30,
|
|
20
38
|
};
|
|
21
39
|
/**
|
|
22
40
|
* Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
|
|
@@ -26,6 +44,9 @@ const DEFAULTS = {
|
|
|
26
44
|
export const DEFAULT_STOP_GRACE_MS = DEFAULTS.STOP_GRACE_MS;
|
|
27
45
|
/** Codex app-server turn idle timeout default (minutes). */
|
|
28
46
|
export const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES = DEFAULTS.TURN_IDLE_TIMEOUT_MINUTES;
|
|
47
|
+
/** 入站媒体默认配置(schema 默认与 connector 兜底共用,避免双源漂移)。 */
|
|
48
|
+
export const DEFAULT_INBOUND_MEDIA_DIR_NAME = '.lark-remote-temp';
|
|
49
|
+
export const DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB = 50;
|
|
29
50
|
/** 模型 ID → alias 映射。 */
|
|
30
51
|
export const MODEL_ID_TO_ALIAS = {
|
|
31
52
|
'claude-opus-4-8': 'opus',
|
|
@@ -47,7 +68,17 @@ const FeishuConfigSchema = z.object({
|
|
|
47
68
|
const ClaudeConfigSchema = z.object({
|
|
48
69
|
model: z.string().default(DEFAULTS.CLAUDE_MODEL),
|
|
49
70
|
effort: z.enum(CLAUDE_EFFORTS).default('medium'),
|
|
50
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Claude 权限模式(官方 --permission-mode 枚举 + 'default'=省略该参数)。
|
|
73
|
+
* 默认 bypassPermissions 保持现有行为(无审批卡);配置为其他值后激活
|
|
74
|
+
* 交互式审批(control_request → run 卡审批区)。
|
|
75
|
+
*/
|
|
76
|
+
permissionMode: z.enum(CLAUDE_PERMISSION_MODES).default('bypassPermissions'),
|
|
77
|
+
/** 审批请求超时(ms),默认 5 分钟;超时自动发送 cancel 并中断 turn。
|
|
78
|
+
* 0 = 立即过期(fail-fast,避免审批永久挂起),不推荐;勿随意改短。 */
|
|
79
|
+
approvalTimeoutMs: z.number().int().min(0).default(DEFAULTS.APPROVAL_TIMEOUT_MS),
|
|
80
|
+
/** 会话级空闲回收(分钟):turn 之间无新消息超过该窗口则停止长驻进程。 */
|
|
81
|
+
idleTtlMinutes: z.number().int().min(0).default(DEFAULTS.CLAUDE_IDLE_TTL_MINUTES),
|
|
51
82
|
stopGraceMs: z.number().int().min(0).default(DEFAULTS.STOP_GRACE_MS),
|
|
52
83
|
});
|
|
53
84
|
/** Codex app-server connection configuration. */
|
|
@@ -75,14 +106,12 @@ export const CodexConfigSchema = z.object({
|
|
|
75
106
|
*/
|
|
76
107
|
// codex 拒绝空串档位(ReasoningEffort::from_str "" → Err,P3-3)
|
|
77
108
|
reasoningEffort: z.string().min(1).optional(),
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/** Sandbox mode (Codex 官方 SandboxMode 标准值). 仅 app-server 模式有效(exec 固定 --sandbox danger-full-access)。 */
|
|
85
|
-
sandbox: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
|
|
109
|
+
/** Approval policy (Codex 官方 AskForApproval 标准值). 默认 on-request。 */
|
|
110
|
+
approvalPolicy: z.enum(['untrusted', 'on-request', 'never']).default('on-request'),
|
|
111
|
+
/** Sandbox mode (Codex 官方 SandboxMode 标准值). 默认 workspace-write。 */
|
|
112
|
+
sandbox: z
|
|
113
|
+
.enum(['read-only', 'workspace-write', 'danger-full-access'])
|
|
114
|
+
.default('workspace-write'),
|
|
86
115
|
/** App-server connection configuration. */
|
|
87
116
|
appServer: CodexAppServerConfigSchema.optional(),
|
|
88
117
|
});
|
|
@@ -92,6 +121,8 @@ const OpencodeConfigSchema = z.object({
|
|
|
92
121
|
providerID: z.string().default('anthropic'),
|
|
93
122
|
/** Model ID for the LLM backend (e.g. 'claude-sonnet-4-20250514'). */
|
|
94
123
|
modelID: z.string().default('claude-sonnet-4-20250514'),
|
|
124
|
+
/** Session mode (opencode agent name): 'build' (default) or 'plan'. */
|
|
125
|
+
mode: z.enum(['build', 'plan']).default('build'),
|
|
95
126
|
});
|
|
96
127
|
/** Pi-specific configuration. pi is a spawn-per-message CLI like Claude. */
|
|
97
128
|
const PiConfigSchema = z.object({
|
|
@@ -104,16 +135,35 @@ const PiConfigSchema = z.object({
|
|
|
104
135
|
/** Comma-separated tool whitelist. */
|
|
105
136
|
tools: z.string().default('read,bash,edit,write,grep,find,ls'),
|
|
106
137
|
});
|
|
107
|
-
/** Kimi-specific configuration.
|
|
138
|
+
/** Kimi-specific configuration. Pure ACP mode (persistent `kimi acp` connection). */
|
|
108
139
|
const KimiConfigSchema = z.object({
|
|
109
140
|
/** Model ID or alias, e.g. 'kimi-code/k3'. */
|
|
110
141
|
model: z.string().default('kimi-code/k3'),
|
|
111
142
|
/** Thinking effort: 'on', 'max'. */
|
|
112
143
|
thinkingEffort: z.enum(KIMI_THINKING_EFFORTS).default('max'),
|
|
144
|
+
/** Permission mode: 'manual' (approve each), 'auto' (engine decides), 'yolo' (allow all). */
|
|
145
|
+
permissionMode: z.enum(['manual', 'auto', 'yolo']).default('manual'),
|
|
146
|
+
/** ACP sub-configuration. */
|
|
147
|
+
acp: z
|
|
148
|
+
.object({
|
|
149
|
+
/** Path to kimi binary for ACP mode. */
|
|
150
|
+
binary: z.string().default('kimi'),
|
|
151
|
+
/** Request timeout in milliseconds. */
|
|
152
|
+
requestTimeoutMs: z.number().int().min(0).default(60000),
|
|
153
|
+
/** Idle TTL for ACP connection in milliseconds. */
|
|
154
|
+
idleTtlMs: z.number().int().min(0).default(1800000),
|
|
155
|
+
/** Turn idle timeout in minutes (0 disables). */
|
|
156
|
+
turnIdleTimeoutMinutes: z.number().int().min(0).default(DEFAULT_TURN_IDLE_TIMEOUT_MINUTES),
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
113
159
|
});
|
|
160
|
+
/** 首次启动的默认 agents 配置:codex 默认 app-server / on-request / workspace-write。 */
|
|
161
|
+
const DEFAULT_AGENTS_CONFIG = {
|
|
162
|
+
codex: CodexConfigSchema.parse({}),
|
|
163
|
+
};
|
|
114
164
|
/** Agents configuration: per-agent settings keyed by agent kind. */
|
|
115
165
|
const AgentsConfigSchema = z.object({
|
|
116
|
-
codex: CodexConfigSchema.
|
|
166
|
+
codex: CodexConfigSchema.default(DEFAULT_AGENTS_CONFIG.codex),
|
|
117
167
|
opencode: OpencodeConfigSchema.optional(),
|
|
118
168
|
pi: PiConfigSchema.optional(),
|
|
119
169
|
kimi: KimiConfigSchema.optional(),
|
|
@@ -134,6 +184,15 @@ const OutputConfigSchema = z.object({
|
|
|
134
184
|
const LoggingConfigSchema = z.object({
|
|
135
185
|
level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
136
186
|
});
|
|
187
|
+
/** 入站媒体落盘配置(预留可配置,默认即可用)。 */
|
|
188
|
+
const InboundMediaConfigSchema = z.object({
|
|
189
|
+
/** 是否启用图片/文件落盘。 */
|
|
190
|
+
enabled: z.boolean().default(true),
|
|
191
|
+
/** 落盘根目录名(位于当前 cwd 下)。 */
|
|
192
|
+
dirName: z.string().min(1).default(DEFAULT_INBOUND_MEDIA_DIR_NAME),
|
|
193
|
+
/** 单文件大小上限(MiB)。 */
|
|
194
|
+
maxFileSizeMb: z.number().int().min(1).default(DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB),
|
|
195
|
+
});
|
|
137
196
|
/** Agent-specific saved choices for quick switching. */
|
|
138
197
|
const AgentChoicesSchema = z.object({
|
|
139
198
|
codex: z
|
|
@@ -166,13 +225,15 @@ export const AppConfigSchema = z.object({
|
|
|
166
225
|
feishu: FeishuConfigSchema,
|
|
167
226
|
/** Claude config (top-level; other agents live under `agents`). */
|
|
168
227
|
claude: ClaudeConfigSchema.default(ClaudeConfigSchema.parse({})),
|
|
169
|
-
/** Per-agent configuration for non-claude agents. */
|
|
170
|
-
agents: AgentsConfigSchema.
|
|
228
|
+
/** Per-agent configuration for non-claude agents. 首次启动默认含 codex(app-server / workspace-write / on-request)。 */
|
|
229
|
+
agents: AgentsConfigSchema.default(DEFAULT_AGENTS_CONFIG),
|
|
171
230
|
/** Agent-specific saved choices for quick switching. */
|
|
172
231
|
agentChoices: AgentChoicesSchema.optional(),
|
|
173
232
|
idle: IdleConfigSchema.default(IdleConfigSchema.parse({})),
|
|
174
233
|
output: OutputConfigSchema.default(OutputConfigSchema.parse({})),
|
|
175
234
|
logging: LoggingConfigSchema.default(LoggingConfigSchema.parse({})),
|
|
235
|
+
/** 入站媒体(图片/文件)落盘配置。 */
|
|
236
|
+
inboundMedia: InboundMediaConfigSchema.default(InboundMediaConfigSchema.parse({})),
|
|
176
237
|
/** Default agent for new runs. */
|
|
177
238
|
defaultAgent: z.enum(['claude', 'codex', 'opencode', 'pi', 'kimi']).default('claude'),
|
|
178
239
|
/** Check for updates on bridge startup (default: false). */
|
|
@@ -185,6 +246,9 @@ const TEMPLATE = `feishu:
|
|
|
185
246
|
claude:
|
|
186
247
|
model: ${DEFAULTS.CLAUDE_MODEL}
|
|
187
248
|
effort: medium
|
|
249
|
+
permissionMode: bypassPermissions
|
|
250
|
+
approvalTimeoutMs: ${DEFAULTS.APPROVAL_TIMEOUT_MS}
|
|
251
|
+
idleTtlMinutes: ${DEFAULTS.CLAUDE_IDLE_TTL_MINUTES}
|
|
188
252
|
stopGraceMs: ${DEFAULTS.STOP_GRACE_MS}
|
|
189
253
|
|
|
190
254
|
idle:
|
|
@@ -272,33 +336,45 @@ export function assertSafeKeyPart(part) {
|
|
|
272
336
|
throw new Error(`[lark-remote] invalid config key segment: '${part}'`);
|
|
273
337
|
}
|
|
274
338
|
}
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
|
|
339
|
+
/**
|
|
340
|
+
* 按 dot-separated key 下钻到目标键的父容器(不含最后一段)。
|
|
341
|
+
*
|
|
342
|
+
* - create=true:中间缺失/非对象段自动补 `{}`(set 语义)
|
|
343
|
+
* - create=false:中间缺失/非对象段返回 undefined(get/delete 语义)
|
|
344
|
+
*
|
|
345
|
+
* 危险段(__proto__/prototype/constructor)在每段统一拒绝。
|
|
346
|
+
*/
|
|
347
|
+
export function walkNestedContainer(target, parts, create) {
|
|
278
348
|
let current = target;
|
|
279
349
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
280
350
|
assertSafeKeyPart(parts[i]);
|
|
281
351
|
const next = current[parts[i]];
|
|
282
352
|
if (next == null || typeof next !== 'object') {
|
|
353
|
+
if (!create)
|
|
354
|
+
return undefined;
|
|
283
355
|
current[parts[i]] = {};
|
|
284
356
|
}
|
|
285
357
|
current = current[parts[i]];
|
|
286
358
|
}
|
|
359
|
+
return current;
|
|
360
|
+
}
|
|
361
|
+
/** 在对象上按 dot-separated key 设置嵌套值(仅修改内存对象,不写盘)。 */
|
|
362
|
+
function setNestedValue(target, key, value) {
|
|
363
|
+
const parts = key.split('.');
|
|
364
|
+
const container = walkNestedContainer(target, parts, true);
|
|
287
365
|
const lastPart = parts[parts.length - 1];
|
|
288
366
|
assertSafeKeyPart(lastPart);
|
|
289
|
-
|
|
367
|
+
container[lastPart] = value;
|
|
290
368
|
}
|
|
291
369
|
/** Get a nested value from config by dot-separated key, e.g. "feishu.appId" */
|
|
292
370
|
export function getConfigValue(config, key) {
|
|
293
371
|
const parts = key.split('.');
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
return current;
|
|
372
|
+
const container = walkNestedContainer(config, parts, false);
|
|
373
|
+
if (!container)
|
|
374
|
+
return undefined;
|
|
375
|
+
const lastPart = parts[parts.length - 1];
|
|
376
|
+
assertSafeKeyPart(lastPart);
|
|
377
|
+
return container[lastPart];
|
|
302
378
|
}
|
|
303
379
|
/** Set a nested value in config by dot-separated key, write back to YAML file */
|
|
304
380
|
export function setConfigValue(configPath, config, key, value) {
|
|
@@ -337,17 +413,12 @@ export function setConfigValues(configPath, config, updates) {
|
|
|
337
413
|
*/
|
|
338
414
|
function deleteNestedValue(target, key) {
|
|
339
415
|
const parts = key.split('.');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const next = current[parts[i]];
|
|
344
|
-
if (next == null || typeof next !== 'object')
|
|
345
|
-
return;
|
|
346
|
-
current = next;
|
|
347
|
-
}
|
|
416
|
+
const container = walkNestedContainer(target, parts, false);
|
|
417
|
+
if (!container)
|
|
418
|
+
return;
|
|
348
419
|
const lastPart = parts[parts.length - 1];
|
|
349
420
|
assertSafeKeyPart(lastPart);
|
|
350
|
-
delete
|
|
421
|
+
delete container[lastPart];
|
|
351
422
|
}
|
|
352
423
|
export function getAgentConfig(config, kind) {
|
|
353
424
|
if (kind === 'claude') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CardActionEvent, type CardActionResponse, type CardStreamProducer } from '@larksuite/channel';
|
|
1
|
+
import { type CardActionEvent, type CardActionResponse, type CardStreamProducer, type LarkChannel } from '@larksuite/channel';
|
|
2
2
|
import type { AppConfig } from '../config/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* dedup 缓存 TTL(毫秒)。控制飞书事件去重窗口,全局作用于 message + cardAction。
|
|
@@ -26,6 +26,54 @@ interface FeishuMessage {
|
|
|
26
26
|
content: string;
|
|
27
27
|
}
|
|
28
28
|
type MessageHandler = (msg: FeishuMessage) => void;
|
|
29
|
+
/**
|
|
30
|
+
* 入站媒体(image/file 消息的 resource)下载结果,交给 bridge 落盘。
|
|
31
|
+
* media 为通过大小限制、可落盘的项;failures 为下载失败或超限的项
|
|
32
|
+
* (bridge 负责把这些失败提示回用户)。
|
|
33
|
+
*/
|
|
34
|
+
export interface InboundMediaItem {
|
|
35
|
+
/** 资源类型:image 消息按 MIME 生成 image_<HHmmss>_<n>.<ext>,file 保留文件名。 */
|
|
36
|
+
type: 'image' | 'file';
|
|
37
|
+
/** 原始文件名(file 消息有;image 消息无,由 bridge 按 MIME 生成)。 */
|
|
38
|
+
fileName?: string;
|
|
39
|
+
/** 服务端 content-type(参数已剥离;可能缺失,bridge 有兜底)。 */
|
|
40
|
+
mimeType?: string;
|
|
41
|
+
/**
|
|
42
|
+
* 下载内容所在临时文件(流式落盘,避免大文件全量进堆内存)。
|
|
43
|
+
* 由 bridge 移动到最终位置;任何未移动路径都必须清理该文件。
|
|
44
|
+
*/
|
|
45
|
+
tempPath: string;
|
|
46
|
+
}
|
|
47
|
+
export interface InboundMediaFailure {
|
|
48
|
+
fileName?: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}
|
|
51
|
+
export interface InboundMediaPayload {
|
|
52
|
+
userId: string;
|
|
53
|
+
chatId: string;
|
|
54
|
+
messageId: string;
|
|
55
|
+
/** 引用上下文:不同 replyTo 强制拆批。 */
|
|
56
|
+
replyToMessageId?: string;
|
|
57
|
+
media: InboundMediaItem[];
|
|
58
|
+
failures: InboundMediaFailure[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 媒体消息到达的轻量描述(未下载):供上层先做 owner/配置闸门,
|
|
62
|
+
* 通过后再调 downloadInboundMedia —— 下载发生在认证之后(P1 review 修复)。
|
|
63
|
+
*/
|
|
64
|
+
export interface InboundMediaMessage {
|
|
65
|
+
userId: string;
|
|
66
|
+
chatId: string;
|
|
67
|
+
messageId: string;
|
|
68
|
+
/** 引用上下文:不同 replyTo 强制拆批。 */
|
|
69
|
+
replyToMessageId?: string;
|
|
70
|
+
resources: Array<{
|
|
71
|
+
type: 'image' | 'file';
|
|
72
|
+
fileKey: string;
|
|
73
|
+
fileName?: string;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
type InboundMediaDetectedHandler = (msg: InboundMediaMessage) => void | Promise<void>;
|
|
29
77
|
/**
|
|
30
78
|
* CardAction handler may return a CardActionResponse (e.g. `{ toast: {...} }`)
|
|
31
79
|
* to give the clicking user native immediate feedback. The SDK passes the
|
|
@@ -34,9 +82,11 @@ type MessageHandler = (msg: FeishuMessage) => void;
|
|
|
34
82
|
*/
|
|
35
83
|
type CardActionHandler = (action: CardActionEvent) => void | CardActionResponse | Promise<void | CardActionResponse>;
|
|
36
84
|
export declare class FeishuConnector {
|
|
37
|
-
|
|
85
|
+
/** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
|
|
86
|
+
channel: LarkChannel;
|
|
38
87
|
private onMessage?;
|
|
39
88
|
private onCardAction?;
|
|
89
|
+
private onInboundMediaDetected?;
|
|
40
90
|
private isConnected;
|
|
41
91
|
private appId;
|
|
42
92
|
private appSecret;
|
|
@@ -44,6 +94,23 @@ export declare class FeishuConnector {
|
|
|
44
94
|
get connected(): boolean;
|
|
45
95
|
setMessageHandler(handler: MessageHandler): void;
|
|
46
96
|
setCardActionHandler(handler: CardActionHandler): void;
|
|
97
|
+
setInboundMediaDetectedHandler(handler: InboundMediaDetectedHandler): void;
|
|
98
|
+
/**
|
|
99
|
+
* 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
|
|
100
|
+
* 再调 downloadInboundMedia。
|
|
101
|
+
*/
|
|
102
|
+
private handleInboundMediaDetected;
|
|
103
|
+
/**
|
|
104
|
+
* 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
|
|
105
|
+
* 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
|
|
106
|
+
* 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
|
|
107
|
+
* 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
|
|
108
|
+
* 当前配置传入(避免 connector 持有过期的 config 快照)。
|
|
109
|
+
*/
|
|
110
|
+
downloadInboundMedia(msg: InboundMediaMessage, opts?: {
|
|
111
|
+
maxFileSizeMb?: number;
|
|
112
|
+
downloadTimeoutMs?: number;
|
|
113
|
+
}): Promise<InboundMediaPayload>;
|
|
47
114
|
connect(): Promise<void>;
|
|
48
115
|
disconnect(): Promise<void>;
|
|
49
116
|
reconnect(): Promise<void>;
|
package/dist/connector/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createLarkChannel, } from '@larksuite/channel';
|
|
2
2
|
import { getLogger } from '../logger/index.js';
|
|
3
3
|
import { MAX_FILE_UPLOAD_SIZE } from './file-limits.js';
|
|
4
|
+
import { DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB } from '../config/index.js';
|
|
5
|
+
import { sleep } from '../common/sleep.js';
|
|
4
6
|
import axios from 'axios';
|
|
5
7
|
import fs from 'node:fs';
|
|
8
|
+
import { silentlyUnlink } from '../common/fs.js';
|
|
6
9
|
import https from 'node:https';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
7
13
|
import FormData from 'form-data';
|
|
8
14
|
/**
|
|
9
15
|
* Dedicated https.Agent with keepAlive disabled for sendFile's three axios
|
|
@@ -36,6 +42,20 @@ const noKeepAliveAgent = new https.Agent({ keepAlive: false });
|
|
|
36
42
|
* 详见 design.md §9.8。原地更新逻辑新增按钮时参考该节去重风险分类。
|
|
37
43
|
*/
|
|
38
44
|
export const DEDUP_TTL_MS = 300;
|
|
45
|
+
/** 单资源下载超时(毫秒):SDK 无内置超时,挂起会导致提示丢失 + 残留临时文件。 */
|
|
46
|
+
const RESOURCE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
47
|
+
function withTimeout(promise, ms, label) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
50
|
+
promise.then((value) => {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
resolve(value);
|
|
53
|
+
}, (err) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
39
59
|
/**
|
|
40
60
|
* 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
|
|
41
61
|
*
|
|
@@ -75,9 +95,11 @@ function tryGetPatchService(channel) {
|
|
|
75
95
|
return c?.im?.v1?.message;
|
|
76
96
|
}
|
|
77
97
|
export class FeishuConnector {
|
|
98
|
+
/** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
|
|
78
99
|
channel;
|
|
79
100
|
onMessage;
|
|
80
101
|
onCardAction;
|
|
102
|
+
onInboundMediaDetected;
|
|
81
103
|
isConnected = false;
|
|
82
104
|
appId;
|
|
83
105
|
appSecret;
|
|
@@ -125,6 +147,15 @@ export class FeishuConnector {
|
|
|
125
147
|
this.channel.on('message', (msg) => {
|
|
126
148
|
if (msg.chatType !== 'p2p')
|
|
127
149
|
return;
|
|
150
|
+
// 入站图片/文件:不转发文本内容(图片消息的 content 只是占位文案),
|
|
151
|
+
// 而是下载资源后交给 bridge 落盘(runner 零改动)。
|
|
152
|
+
if ((msg.rawContentType === 'image' || msg.rawContentType === 'file') &&
|
|
153
|
+
msg.resources.length > 0) {
|
|
154
|
+
// 只上报"媒体到达",不在此下载:owner/配置闸门在 index.ts 里先执行,
|
|
155
|
+
// 通过后才调用 downloadInboundMedia(避免未认证大文件下载打爆内存)。
|
|
156
|
+
void this.handleInboundMediaDetected(msg);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
128
159
|
this.onMessage?.({
|
|
129
160
|
userId: msg.senderId,
|
|
130
161
|
messageId: msg.messageId,
|
|
@@ -158,6 +189,81 @@ export class FeishuConnector {
|
|
|
158
189
|
setCardActionHandler(handler) {
|
|
159
190
|
this.onCardAction = handler;
|
|
160
191
|
}
|
|
192
|
+
setInboundMediaDetectedHandler(handler) {
|
|
193
|
+
this.onInboundMediaDetected = handler;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
|
|
197
|
+
* 再调 downloadInboundMedia。
|
|
198
|
+
*/
|
|
199
|
+
async handleInboundMediaDetected(msg) {
|
|
200
|
+
if (!this.onInboundMediaDetected) {
|
|
201
|
+
getLogger().warn('[feishu] inbound media received but no detected handler registered');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await this.onInboundMediaDetected({
|
|
206
|
+
userId: msg.senderId,
|
|
207
|
+
chatId: msg.chatId,
|
|
208
|
+
messageId: msg.messageId,
|
|
209
|
+
replyToMessageId: msg.replyToMessageId,
|
|
210
|
+
resources: msg.resources.map((r) => ({
|
|
211
|
+
type: r.type === 'image' ? 'image' : 'file',
|
|
212
|
+
fileKey: r.fileKey,
|
|
213
|
+
fileName: r.fileName,
|
|
214
|
+
})),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
getLogger().error('[feishu] inbound media detected handler failed:', err.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
|
|
223
|
+
* 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
|
|
224
|
+
* 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
|
|
225
|
+
* 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
|
|
226
|
+
* 当前配置传入(避免 connector 持有过期的 config 快照)。
|
|
227
|
+
*/
|
|
228
|
+
async downloadInboundMedia(msg, opts) {
|
|
229
|
+
const maxBytes = (opts?.maxFileSizeMb ?? DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB) * 1024 * 1024;
|
|
230
|
+
const timeoutMs = opts?.downloadTimeoutMs ?? RESOURCE_DOWNLOAD_TIMEOUT_MS;
|
|
231
|
+
const media = [];
|
|
232
|
+
const failures = [];
|
|
233
|
+
for (const res of msg.resources) {
|
|
234
|
+
const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
|
|
235
|
+
try {
|
|
236
|
+
const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`);
|
|
237
|
+
if (bytesWritten > maxBytes) {
|
|
238
|
+
silentlyUnlink(tmpPath);
|
|
239
|
+
failures.push({
|
|
240
|
+
fileName: res.fileName,
|
|
241
|
+
reason: `超过 ${maxBytes / (1024 * 1024)}MB 大小限制`,
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
media.push({
|
|
246
|
+
type: res.type,
|
|
247
|
+
fileName: res.fileName,
|
|
248
|
+
mimeType: contentType,
|
|
249
|
+
tempPath: tmpPath,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
silentlyUnlink(tmpPath);
|
|
254
|
+
getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
|
|
255
|
+
failures.push({ fileName: res.fileName, reason: `下载失败: ${err.message}` });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
userId: msg.userId,
|
|
260
|
+
chatId: msg.chatId,
|
|
261
|
+
messageId: msg.messageId,
|
|
262
|
+
replyToMessageId: msg.replyToMessageId,
|
|
263
|
+
media,
|
|
264
|
+
failures,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
161
267
|
async connect() {
|
|
162
268
|
try {
|
|
163
269
|
await this.channel.connect();
|
|
@@ -200,7 +306,7 @@ export class FeishuConnector {
|
|
|
200
306
|
catch (err) {
|
|
201
307
|
if (shouldRetrySendError(err)) {
|
|
202
308
|
getLogger().warn('[feishu] rate limited, retrying in 200ms...');
|
|
203
|
-
await
|
|
309
|
+
await sleep(200);
|
|
204
310
|
try {
|
|
205
311
|
const result = await this.channel.send(chatId, input, { replyTo: opts?.replyTo });
|
|
206
312
|
return result.messageId;
|