lark-remote 0.5.10 → 0.5.13
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 +51 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/binder.js +7 -1
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +89 -230
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/approval-render.d.ts +14 -0
- package/dist/card/approval-render.js +155 -29
- package/dist/card/bash-card-session.js +10 -5
- package/dist/card/bash-renderer.js +11 -23
- package/dist/card/card-budget.js +24 -4
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +150 -10
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/common/text-width.js +5 -0
- package/dist/common/tool-summary.js +4 -2
- package/dist/config/codex-config.js +5 -3
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +19 -5
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -391
- package/dist/instance-lock.d.ts +9 -1
- package/dist/instance-lock.js +42 -7
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/platform/identity.js +4 -3
- package/dist/platform/path.js +3 -1
- package/dist/platform/terminator-win32.d.ts +13 -1
- package/dist/platform/terminator-win32.js +4 -14
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +93 -1133
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/runner.js +12 -0
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +15 -8
- package/dist/runner/common/acp/base-translator.js +38 -7
- package/dist/runner/common/jsonl-stream.js +12 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
- package/dist/runner/common/jsonrpc/transport.js +7 -1
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/approval.js +16 -3
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +32 -7
- package/dist/runner/dsh/client.js +115 -149
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +63 -24
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +42 -5
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +42 -5
- package/dist/runner/kimi/acp/translator.d.ts +8 -0
- package/dist/runner/kimi/acp/translator.js +11 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/runner.js +33 -5
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/resolve-agent-choices.js +5 -1
- package/dist/runner/sync-agent-choices.js +4 -1
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +13 -4
- package/dist/session/codex/rollout-reader.js +51 -26
- package/dist/session/common/usage-accumulator.d.ts +10 -1
- package/dist/session/common/usage-accumulator.js +10 -2
- package/dist/session/dsh/sessions.js +29 -9
- package/dist/session/kimi/sessions.js +11 -7
- package/dist/session/opencode/sessions.js +15 -7
- package/dist/session/pi/sessions.js +3 -2
- package/dist/session/session-store.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { COMPACT_ACTION_CMDS, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, isImmediateAction, } from './router/index.js';
|
|
3
|
+
import { InboundTurnAssembler } from './inbound/turn-assembler.js';
|
|
4
|
+
import { InboundAuthorizer } from './inbound/authorize.js';
|
|
5
|
+
import { stripPlaceholders } from './inbound/placeholder.js';
|
|
6
|
+
import { agentDisplayName } from './card/card-shared.js';
|
|
7
|
+
import { sleep } from './common/sleep.js';
|
|
8
|
+
import { bestEffortUnlink } from './common/fs.js';
|
|
9
|
+
import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
|
|
10
|
+
import { buildCardActionFullValue } from './router/card-action-payload.js';
|
|
11
|
+
import { actionFeedbackText } from './router/card-action-feedback.js';
|
|
12
|
+
const SESSION_MUTATING_FLUSH_BUDGET_MS = 2_000;
|
|
13
|
+
/**
|
|
14
|
+
* /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
|
|
15
|
+
* 共用同一判定,防止将来加别名时两处漂移。
|
|
16
|
+
*/
|
|
17
|
+
export function isStopCommand(text) {
|
|
18
|
+
const t = text.trim().toLowerCase();
|
|
19
|
+
return t === '/stop' || t === '/t';
|
|
20
|
+
}
|
|
21
|
+
export function setupMessageHandlers(deps) {
|
|
22
|
+
const { connector, router, bridge, sessionStore, workspaceStore, binder, logger, config, cloneSession, } = deps;
|
|
23
|
+
// 入站统一装配器:图/文任意顺序 → 同一个 turn、同一个 prompt。
|
|
24
|
+
const assembler = new InboundTurnAssembler({
|
|
25
|
+
onCommit: (turn, prompt) => {
|
|
26
|
+
const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
|
|
27
|
+
const ctx = {
|
|
28
|
+
userId: turn.userId,
|
|
29
|
+
chatId: turn.chatId,
|
|
30
|
+
messageId,
|
|
31
|
+
turnMessageIds: turn.messageIds,
|
|
32
|
+
};
|
|
33
|
+
let cwd = sessionStore.getCwd(turn.userId) ?? '';
|
|
34
|
+
if (!cwd) {
|
|
35
|
+
const recent = [...workspaceStore.list()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
36
|
+
if (recent.length > 0)
|
|
37
|
+
cwd = recent[0].path;
|
|
38
|
+
}
|
|
39
|
+
const binding = bridge.currentBinding(turn.userId);
|
|
40
|
+
bridge.enqueue(cwd, async () => {
|
|
41
|
+
await router.handle(prompt, ctx, {
|
|
42
|
+
cwdOverride: cwd,
|
|
43
|
+
binding,
|
|
44
|
+
allowCommandPrefix: false,
|
|
45
|
+
});
|
|
46
|
+
}, {
|
|
47
|
+
taskMeta: {
|
|
48
|
+
userId: turn.userId,
|
|
49
|
+
chatId: turn.chatId,
|
|
50
|
+
messageId,
|
|
51
|
+
messagePreview: prompt.slice(0, 3000),
|
|
52
|
+
binding,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
onReceipt: async (ctx, text) => {
|
|
57
|
+
await bridge.sendResult({ text }, ctx);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
router.setInboundFlusher(() => assembler.flushAll('flush'));
|
|
61
|
+
const authorizer = new InboundAuthorizer({
|
|
62
|
+
binder,
|
|
63
|
+
onBind: (userId, chatId) => {
|
|
64
|
+
let resolvedCwd;
|
|
65
|
+
try {
|
|
66
|
+
resolvedCwd = fs.realpathSync(process.cwd());
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
resolvedCwd = process.cwd();
|
|
70
|
+
}
|
|
71
|
+
if (!sessionStore.getCwd(userId)) {
|
|
72
|
+
sessionStore.setCwd(userId, resolvedCwd);
|
|
73
|
+
}
|
|
74
|
+
return (async () => {
|
|
75
|
+
try {
|
|
76
|
+
await connector.sendWithRetry(chatId, {
|
|
77
|
+
text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
|
|
78
|
+
`📂 当前工作目录: \`${resolvedCwd}\`\n` +
|
|
79
|
+
`🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
|
|
80
|
+
`💡 直接输入消息即可开始对话,或 /cd 切换目录`,
|
|
81
|
+
});
|
|
82
|
+
const helpResult = router.cmdHelp();
|
|
83
|
+
if (helpResult.card) {
|
|
84
|
+
await connector.sendWithRetry(chatId, { card: helpResult.card });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
logger.error('[binder] onboarding send failed:', err);
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
connector.setInboundMediaDetectedHandler((msg) => {
|
|
94
|
+
const authorization = authorizer.authorize(msg.userId, msg.chatId);
|
|
95
|
+
if (authorization === 'rejected') {
|
|
96
|
+
logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (cloneSession.isActive()) {
|
|
100
|
+
logger.info(`[media] dropped inbound media during clone session ${msg.messageId}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!router.config.inboundMedia.enabled) {
|
|
104
|
+
assembler.ingest({
|
|
105
|
+
kind: 'rejected',
|
|
106
|
+
userId: msg.userId,
|
|
107
|
+
chatId: msg.chatId,
|
|
108
|
+
messageId: msg.messageId,
|
|
109
|
+
replyToMessageId: msg.replyToMessageId,
|
|
110
|
+
rawContentType: msg.rawContentType,
|
|
111
|
+
rejectedKind: 'file',
|
|
112
|
+
reason: '入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
|
|
113
|
+
'如需自动保存图片/文件,请开启后重试',
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const outcome = (async () => {
|
|
118
|
+
const payload = await connector.downloadInboundMedia(msg, {
|
|
119
|
+
maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
return await bridge.saveInboundMedia(payload);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
for (const item of payload.media)
|
|
126
|
+
bestEffortUnlink(item.tempPath);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
assembler.ingest({
|
|
131
|
+
kind: 'media',
|
|
132
|
+
userId: msg.userId,
|
|
133
|
+
chatId: msg.chatId,
|
|
134
|
+
messageId: msg.messageId,
|
|
135
|
+
replyToMessageId: msg.replyToMessageId,
|
|
136
|
+
rawContentType: msg.rawContentType,
|
|
137
|
+
outcome,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
connector.setMessageHandler((msg) => {
|
|
141
|
+
const decision = authorizer.authorize(msg.userId, msg.chatId);
|
|
142
|
+
if (decision === 'rejected' || decision === 'bind_success')
|
|
143
|
+
return;
|
|
144
|
+
logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
|
|
145
|
+
void connector.addReaction(msg.messageId, 'Typing');
|
|
146
|
+
if (cloneSession.isActive() && !isStopCommand(msg.content)) {
|
|
147
|
+
void cloneSession
|
|
148
|
+
.handleMessage(msg.content, {
|
|
149
|
+
userId: msg.userId,
|
|
150
|
+
chatId: msg.chatId,
|
|
151
|
+
messageId: msg.messageId,
|
|
152
|
+
})
|
|
153
|
+
.catch((err) => logger.error('[clone] message handling failed:', err));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const content = router.expandAliasMessage(msg.content);
|
|
157
|
+
if (isStopCommand(content)) {
|
|
158
|
+
void (async () => {
|
|
159
|
+
const interrupt = await bridge.interruptCurrentRunDetailed({
|
|
160
|
+
userId: msg.userId,
|
|
161
|
+
chatId: msg.chatId,
|
|
162
|
+
});
|
|
163
|
+
const ctx = { userId: msg.userId, chatId: msg.chatId, messageId: msg.messageId };
|
|
164
|
+
if (!interrupt.stopped) {
|
|
165
|
+
await bridge.sendResult({ text: '当前没有运行中的进程' }, ctx);
|
|
166
|
+
}
|
|
167
|
+
else if ((interrupt.stoppedCount ?? 1) > 1) {
|
|
168
|
+
await bridge.sendResult({ text: `✅ 已停止 ${interrupt.stoppedCount} 个运行中的任务` }, ctx);
|
|
169
|
+
}
|
|
170
|
+
})().catch((err) => logger.error('[control] /stop failed:', err));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const stripped = stripPlaceholders(content);
|
|
174
|
+
if (stripped.unknownTags.length > 0) {
|
|
175
|
+
logger.warn(`[inbound] unknown placeholder tags stripped: ${stripped.unknownTags.join(', ')} ` +
|
|
176
|
+
`(msg_type=${msg.rawContentType})`);
|
|
177
|
+
}
|
|
178
|
+
const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
|
|
179
|
+
if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
|
|
180
|
+
void (async () => {
|
|
181
|
+
await assembler.flush(msg.userId, msg.chatId, 'flush');
|
|
182
|
+
await router.handle(stripped.clean, {
|
|
183
|
+
userId: msg.userId,
|
|
184
|
+
chatId: msg.chatId,
|
|
185
|
+
messageId: msg.messageId,
|
|
186
|
+
}, { allowCommandPrefix: true });
|
|
187
|
+
})().catch((err) => logger.error('[control] command failed:', err));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
assembler.ingest({
|
|
191
|
+
kind: 'text',
|
|
192
|
+
userId: msg.userId,
|
|
193
|
+
chatId: msg.chatId,
|
|
194
|
+
messageId: msg.messageId,
|
|
195
|
+
replyToMessageId: msg.replyToMessageId,
|
|
196
|
+
rawContentType: msg.rawContentType,
|
|
197
|
+
text: stripped.clean,
|
|
198
|
+
placeholders: stripped.kinds,
|
|
199
|
+
unknownTags: stripped.unknownTags,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
connector.setCardActionHandler(async (action) => {
|
|
203
|
+
if (!binder.isOwner(action.operator.openId)) {
|
|
204
|
+
binder.recordRejectedCardAction(action.operator.openId);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const actionValue = action.action.value;
|
|
208
|
+
if (!actionValue?.cmd)
|
|
209
|
+
return;
|
|
210
|
+
logger.info(`card action: ${actionValue.cmd}`);
|
|
211
|
+
const userId = action.operator.openId;
|
|
212
|
+
const chatId = action.chatId;
|
|
213
|
+
const messageId = action.messageId;
|
|
214
|
+
void connector.addReaction(messageId, 'Typing');
|
|
215
|
+
if (actionValue.cmd === 'stop') {
|
|
216
|
+
void router
|
|
217
|
+
.handleCardAction({ ...actionValue, cmd: 'stop' }, { userId, chatId, messageId })
|
|
218
|
+
.catch((err) => logger.error('[control] card stop failed:', err));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const isImmediate = isImmediateAction(actionValue.cmd);
|
|
222
|
+
const cwd = sessionStore.getCwd(userId) ?? '';
|
|
223
|
+
const fullValue = buildCardActionFullValue(actionValue, action);
|
|
224
|
+
if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
|
|
225
|
+
const flushP = assembler
|
|
226
|
+
.flush(userId, chatId, 'flush')
|
|
227
|
+
.catch((err) => logger.error('[control] inbound flush failed:', err));
|
|
228
|
+
await Promise.race([flushP, sleep(SESSION_MUTATING_FLUSH_BUDGET_MS)]);
|
|
229
|
+
}
|
|
230
|
+
if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
|
|
231
|
+
return router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
232
|
+
}
|
|
233
|
+
if (actionValue.cmd === 'order.exec') {
|
|
234
|
+
void dispatchOrderExecForQueue({
|
|
235
|
+
router,
|
|
236
|
+
bridge,
|
|
237
|
+
cwd,
|
|
238
|
+
orderId: actionValue.orderId,
|
|
239
|
+
ctx: { userId, chatId, messageId },
|
|
240
|
+
}).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const forwardActionFeedback = (res) => {
|
|
244
|
+
const text = actionFeedbackText(res);
|
|
245
|
+
if (!text)
|
|
246
|
+
return;
|
|
247
|
+
void bridge
|
|
248
|
+
.sendResult({ text }, { userId, chatId, messageId })
|
|
249
|
+
.catch((err) => logger.error('[control] card action feedback failed:', err));
|
|
250
|
+
};
|
|
251
|
+
if (isImmediate) {
|
|
252
|
+
bridge.enqueueImmediate(cwd, async () => {
|
|
253
|
+
const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
254
|
+
forwardActionFeedback(res);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
bridge.enqueue(cwd, async () => {
|
|
259
|
+
const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
|
|
260
|
+
forwardActionFeedback(res);
|
|
261
|
+
}, {
|
|
262
|
+
taskMeta: {
|
|
263
|
+
userId,
|
|
264
|
+
chatId,
|
|
265
|
+
messageId: router.mintQueueTaskKey('card', actionValue.cmd),
|
|
266
|
+
feishuReplyTo: messageId,
|
|
267
|
+
messagePreview: `card action: ${actionValue.cmd}`,
|
|
268
|
+
kind: COMPACT_ACTION_CMDS.has(actionValue.cmd) ? 'compact' : 'command',
|
|
269
|
+
editable: !COMPACT_ACTION_CMDS.has(actionValue.cmd),
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=message-handlers.js.map
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* (killOrphan)应按需调用,真机矩阵需实测整体耗时;
|
|
13
13
|
* - platform 显式注入,win32 语义可在任意宿主上单测。
|
|
14
14
|
*/
|
|
15
|
-
import { execFileSync, spawn } from 'node:child_process';
|
|
16
15
|
import path from 'node:path';
|
|
17
16
|
import { currentPlatform, isWin32 } from './select.js';
|
|
17
|
+
import { spawnProcess, spawnProcessSync } from './spawn.js';
|
|
18
18
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
19
19
|
/** 拉起一次性查询进程并收全 stdout;超时/拉起失败/进程报错一律 null。 */
|
|
20
20
|
function collect(file, args, timeoutMs) {
|
|
@@ -24,7 +24,7 @@ function collect(file, args, timeoutMs) {
|
|
|
24
24
|
// stderr 用 ignore:查询进程的错误输出我们不需要,pipe 而不消费在极端
|
|
25
25
|
// 情况下(管道缓冲满)会阻塞子进程,把一次性查询拖成挂起。
|
|
26
26
|
// windowsHide:避免查询进程在桌面闪控制台窗口
|
|
27
|
-
proc =
|
|
27
|
+
proc = spawnProcess(file, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
28
28
|
}
|
|
29
29
|
catch {
|
|
30
30
|
resolve(null);
|
|
@@ -243,12 +243,13 @@ export function verifyPidIdentityVerdictSync(pid, opts) {
|
|
|
243
243
|
return 'unknown';
|
|
244
244
|
let stdout;
|
|
245
245
|
try {
|
|
246
|
-
|
|
246
|
+
const res = spawnProcessSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
247
247
|
encoding: 'utf-8',
|
|
248
248
|
timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
249
249
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
250
250
|
windowsHide: true,
|
|
251
251
|
});
|
|
252
|
+
stdout = String(res.stdout ?? '');
|
|
252
253
|
}
|
|
253
254
|
catch {
|
|
254
255
|
// 进程不存在 / ps 不可用 / 超时:查不到身份 → unknown(不杀)
|
package/dist/platform/path.js
CHANGED
|
@@ -21,8 +21,10 @@ function resolveOptions(opts) {
|
|
|
21
21
|
*/
|
|
22
22
|
export function pathKey(p, opts) {
|
|
23
23
|
const { platform } = resolveOptions(opts);
|
|
24
|
+
// linux:剥尾部分隔符,与 darwin/win32 同口径(否则 `/cd ~/repo/` 与
|
|
25
|
+
// `/cd ~/repo` 会被判成两个目录——darwin 分支注释写明的不变量)。
|
|
24
26
|
if (platform === 'linux')
|
|
25
|
-
return p;
|
|
27
|
+
return p.replace(/\/+$/, '');
|
|
26
28
|
if (isWin32(platform)) {
|
|
27
29
|
// win32:\ 与 / 都是分隔符,且路径大小写不敏感
|
|
28
30
|
return p.replaceAll('\\', '/').replace(/\/+$/, '').toLowerCase();
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀。
|
|
3
|
+
*
|
|
4
|
+
* 状态机:
|
|
5
|
+
* 1. 进程已退出 → nothing(via='already-exited')
|
|
6
|
+
* 2. immediate → taskkill /PID <pid> /T /F
|
|
7
|
+
* 3. 优雅 → AgentStopperRegistry 查协议通道
|
|
8
|
+
* - 无通道:打日志显式跳过优雅段(不等一个不会来的退出事件)→ 直接树杀
|
|
9
|
+
* - 有通道:调用后与 exit 事件竞速等 graceMs,仍存活 → 树杀
|
|
10
|
+
*
|
|
11
|
+
* 依赖(spawn / 等待器 / 日志)全部构造注入,win32 语义可在任意宿主上单测。
|
|
12
|
+
*/
|
|
1
13
|
import type { ChildProcess, SpawnOptions } from 'node:child_process';
|
|
2
14
|
import type { Terminator } from './terminator.js';
|
|
3
15
|
import { AgentStopperRegistry } from './agent-stopper.js';
|
|
@@ -11,7 +23,7 @@ export interface Win32TerminatorDeps {
|
|
|
11
23
|
agent?: string;
|
|
12
24
|
/** 协议通道注册表注入(测试隔离);默认进程级单例 */
|
|
13
25
|
stoppers?: AgentStopperRegistry;
|
|
14
|
-
/** spawn 注入(测试用);默认
|
|
26
|
+
/** spawn 注入(测试用);默认 platform/spawn.ts 的 spawnProcess */
|
|
15
27
|
spawn?: SpawnLike;
|
|
16
28
|
/** grace 等待器注入(测试用);默认与 exit 事件竞速 */
|
|
17
29
|
waitForExit?: WaitForExit;
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀。
|
|
3
|
-
*
|
|
4
|
-
* 状态机:
|
|
5
|
-
* 1. 进程已退出 → nothing(via='already-exited')
|
|
6
|
-
* 2. immediate → taskkill /PID <pid> /T /F
|
|
7
|
-
* 3. 优雅 → AgentStopperRegistry 查协议通道
|
|
8
|
-
* - 无通道:打日志显式跳过优雅段(不等一个不会来的退出事件)→ 直接树杀
|
|
9
|
-
* - 有通道:调用后与 exit 事件竞速等 graceMs,仍存活 → 树杀
|
|
10
|
-
*
|
|
11
|
-
* 依赖(spawn / 等待器 / 日志)全部构造注入,win32 语义可在任意宿主上单测。
|
|
12
|
-
*/
|
|
13
|
-
import { spawn as nodeSpawn } from 'node:child_process';
|
|
14
1
|
import { agentStopperRegistry } from './agent-stopper.js';
|
|
2
|
+
import { spawnProcess } from './spawn.js';
|
|
15
3
|
/** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
|
|
16
4
|
function isAlive(proc) {
|
|
17
5
|
return proc.exitCode === null && proc.signalCode === null;
|
|
@@ -36,7 +24,9 @@ async function defaultWaitForExit(proc, ms) {
|
|
|
36
24
|
});
|
|
37
25
|
}
|
|
38
26
|
export function createWin32Terminator(deps) {
|
|
39
|
-
|
|
27
|
+
// 默认走 platform/spawn.ts 单点收口:taskkill 是系统二进制,
|
|
28
|
+
// cross-spawn 在 win32 之外直通、在 win32 垫片场景下受控执行。
|
|
29
|
+
const spawnFn = deps.spawn ?? spawnProcess;
|
|
40
30
|
const stoppers = deps.stoppers ?? agentStopperRegistry;
|
|
41
31
|
const waitForExit = deps.waitForExit ?? defaultWaitForExit;
|
|
42
32
|
const log = deps.log;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* out-of-band fields.
|
|
10
10
|
*
|
|
11
11
|
* Extracted from src/index.ts so the entry transform is behavior-testable
|
|
12
|
-
* (
|
|
12
|
+
* (see message-handlers.ts / message-handlers.test.ts).
|
|
13
13
|
*/
|
|
14
14
|
import type { CardActionEvent } from '@larksuite/channel';
|
|
15
15
|
import type { CardActionPayload } from './index.js';
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* out-of-band fields.
|
|
10
10
|
*
|
|
11
11
|
* Extracted from src/index.ts so the entry transform is behavior-testable
|
|
12
|
-
* (
|
|
12
|
+
* (see message-handlers.ts / message-handlers.test.ts).
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
15
|
* Build the full card action payload for router.handleCardAction.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommandContext, CommandResult } from '../../bridge/index.js';
|
|
2
|
+
export declare const LS_PAGE_SIZE = 30;
|
|
3
|
+
export interface LsCommandOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
offset?: number;
|
|
6
|
+
rootDir?: string;
|
|
7
|
+
q?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildLsCommand(args: string[], _ctx: CommandContext, options: LsCommandOptions): CommandResult;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { cardButton } from '../../card/card-shared.js';
|
|
5
|
+
import { paginationBar, searchBar } from '../card-helpers.js';
|
|
6
|
+
import { inspectPath, statPath } from '../path-validation.js';
|
|
7
|
+
import { pageSlice } from '../paging.js';
|
|
8
|
+
export const LS_PAGE_SIZE = 30;
|
|
9
|
+
function formatBytes(bytes) {
|
|
10
|
+
if (bytes < 1024)
|
|
11
|
+
return `${bytes}B`;
|
|
12
|
+
if (bytes < 1024 * 1024)
|
|
13
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
14
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
15
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
16
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
17
|
+
}
|
|
18
|
+
function buildFileCard(filePath) {
|
|
19
|
+
const resolution = statPath(filePath);
|
|
20
|
+
const stat = resolution.ok ? resolution.stat : undefined;
|
|
21
|
+
const sizeText = stat ? formatBytes(stat.size) : '未知';
|
|
22
|
+
const mtimeText = stat ? stat.mtime.toLocaleString('zh-CN', { hour12: false }) : '未知';
|
|
23
|
+
const parentDir = path.dirname(filePath);
|
|
24
|
+
const buttons = [
|
|
25
|
+
cardButton('📎 下载', { cmd: 'ls.file', path: filePath }, { type: 'primary', size: 'small' }),
|
|
26
|
+
];
|
|
27
|
+
if (parentDir !== filePath) {
|
|
28
|
+
buttons.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: parentDir }, { type: 'default', size: 'small' }));
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
card: {
|
|
32
|
+
schema: '2.0',
|
|
33
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
34
|
+
header: {
|
|
35
|
+
title: { tag: 'plain_text', content: `📄 ${path.basename(filePath)}` },
|
|
36
|
+
template: 'blue',
|
|
37
|
+
},
|
|
38
|
+
body: {
|
|
39
|
+
elements: [
|
|
40
|
+
{ tag: 'div', text: { tag: 'lark_md', content: `\`${filePath}\`\n${sizeText}` } },
|
|
41
|
+
{ tag: 'div', text: { tag: 'lark_md', content: `修改时间: ${mtimeText}` } },
|
|
42
|
+
{
|
|
43
|
+
tag: 'column_set',
|
|
44
|
+
columns: buttons.map((button) => ({
|
|
45
|
+
tag: 'column',
|
|
46
|
+
width: 'auto',
|
|
47
|
+
elements: [button],
|
|
48
|
+
})),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function buildLsCommand(args, _ctx, options) {
|
|
56
|
+
const { cwd, offset = 0, rootDir } = options;
|
|
57
|
+
if (!cwd)
|
|
58
|
+
return { text: '请先使用 /cd <path> 设置工作目录' };
|
|
59
|
+
const filter = options.q?.trim() || undefined;
|
|
60
|
+
let targetDir = cwd;
|
|
61
|
+
if (args[0]) {
|
|
62
|
+
const arg = args[0].startsWith('~') ? path.join(os.homedir(), args[0].slice(1)) : args[0];
|
|
63
|
+
const potentialDir = path.resolve(cwd, arg);
|
|
64
|
+
const kind = inspectPath(potentialDir);
|
|
65
|
+
if (kind === 'missing' || kind === 'unreadable') {
|
|
66
|
+
return { text: `ls: ${args[0]}: No such file or directory` };
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'file')
|
|
69
|
+
return buildFileCard(potentialDir);
|
|
70
|
+
targetDir = potentialDir;
|
|
71
|
+
}
|
|
72
|
+
const browseRoot = rootDir ? path.resolve(cwd, rootDir) : targetDir;
|
|
73
|
+
try {
|
|
74
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
75
|
+
const dirs = [];
|
|
76
|
+
const files = [];
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
let isDir = entry.isDirectory();
|
|
79
|
+
let size;
|
|
80
|
+
if (!isDir) {
|
|
81
|
+
const resolution = statPath(path.join(targetDir, entry.name));
|
|
82
|
+
if (resolution.ok) {
|
|
83
|
+
isDir = resolution.stat.isDirectory();
|
|
84
|
+
if (!isDir)
|
|
85
|
+
size = resolution.stat.size;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (isDir)
|
|
89
|
+
dirs.push(entry.name);
|
|
90
|
+
else
|
|
91
|
+
files.push({ name: entry.name, size });
|
|
92
|
+
}
|
|
93
|
+
dirs.sort((a, b) => a.localeCompare(b));
|
|
94
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
+
const allItems = [
|
|
96
|
+
...dirs.map((name) => ({ name, isDir: true })),
|
|
97
|
+
...files.map((file) => ({ name: file.name, isDir: false, size: file.size })),
|
|
98
|
+
];
|
|
99
|
+
const ql = filter?.toLowerCase();
|
|
100
|
+
const visibleItems = ql
|
|
101
|
+
? allItems.filter((item) => item.name.toLowerCase().includes(ql))
|
|
102
|
+
: allItems;
|
|
103
|
+
const visibleDirCount = visibleItems.reduce((count, item) => count + (item.isDir ? 1 : 0), 0);
|
|
104
|
+
const visibleFileCount = visibleItems.length - visibleDirCount;
|
|
105
|
+
const { totalCount, totalPages, safeOffset, currentPage, pageItems, hasPagination } = pageSlice(visibleItems, offset, LS_PAGE_SIZE);
|
|
106
|
+
const parentDir = path.dirname(targetDir);
|
|
107
|
+
const headerElements = [];
|
|
108
|
+
if (parentDir !== targetDir) {
|
|
109
|
+
headerElements.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: browseRoot }, { type: 'default', size: 'small' }));
|
|
110
|
+
}
|
|
111
|
+
if (targetDir !== browseRoot) {
|
|
112
|
+
headerElements.push(cardButton('返回', { cmd: 'ls.browse', path: browseRoot, root: browseRoot }, { type: 'default', size: 'small' }));
|
|
113
|
+
}
|
|
114
|
+
headerElements.push(cardButton('刷新', {
|
|
115
|
+
cmd: 'ls.refresh',
|
|
116
|
+
path: targetDir,
|
|
117
|
+
offset: safeOffset,
|
|
118
|
+
root: browseRoot,
|
|
119
|
+
...(filter ? { q: filter } : {}),
|
|
120
|
+
}, { type: 'default', size: 'small' }));
|
|
121
|
+
if (targetDir !== cwd) {
|
|
122
|
+
headerElements.push(cardButton('切换', { cmd: 'ls.switch', path: targetDir, root: browseRoot }, { type: 'primary', size: 'small' }));
|
|
123
|
+
}
|
|
124
|
+
const dirButton = (name) => cardButton(`📁 ${name}`, { cmd: 'ls.browse', path: path.join(targetDir, name), root: browseRoot }, { type: 'default' });
|
|
125
|
+
const fileButton = (name, size) => cardButton(`📄 ${name}${size !== undefined ? ` (${formatBytes(size)})` : ''}`, { cmd: 'ls.file', path: path.join(targetDir, name) }, { type: 'default', size: 'small' });
|
|
126
|
+
const elements = [
|
|
127
|
+
{
|
|
128
|
+
tag: 'div',
|
|
129
|
+
text: {
|
|
130
|
+
tag: 'lark_md',
|
|
131
|
+
content: `\`${targetDir}\`\n共 ${visibleDirCount} 目录, ${visibleFileCount} 文件`,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
tag: 'column_set',
|
|
136
|
+
columns: headerElements.map((element) => ({
|
|
137
|
+
tag: 'column',
|
|
138
|
+
width: 'auto',
|
|
139
|
+
elements: [element],
|
|
140
|
+
})),
|
|
141
|
+
},
|
|
142
|
+
{ tag: 'hr' },
|
|
143
|
+
searchBar({
|
|
144
|
+
cmd: 'ls.filter',
|
|
145
|
+
placeholder: '搜索本层目录/文件名,输完回车',
|
|
146
|
+
currentQuery: filter,
|
|
147
|
+
extra: { path: targetDir, root: browseRoot },
|
|
148
|
+
}),
|
|
149
|
+
];
|
|
150
|
+
if (filter) {
|
|
151
|
+
elements.push({
|
|
152
|
+
tag: 'div',
|
|
153
|
+
text: {
|
|
154
|
+
tag: 'lark_md',
|
|
155
|
+
content: `🔍 筛选:"${filter}" · 命中 ${totalCount} 项(点「清除筛选」取消)`,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const pageDirs = pageItems.filter((item) => item.isDir);
|
|
160
|
+
const pageFiles = pageItems.filter((item) => !item.isDir);
|
|
161
|
+
if (filter && visibleItems.length === 0) {
|
|
162
|
+
elements.push({
|
|
163
|
+
tag: 'div',
|
|
164
|
+
text: { tag: 'lark_md', content: `🔍 无匹配条目(关键词:"${filter}")` },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
elements.push({
|
|
169
|
+
tag: 'div',
|
|
170
|
+
text: { tag: 'lark_md', content: `**📂 目录 (${visibleDirCount})**` },
|
|
171
|
+
});
|
|
172
|
+
if (pageDirs.length > 0) {
|
|
173
|
+
for (const dir of pageDirs) {
|
|
174
|
+
elements.push({
|
|
175
|
+
tag: 'column_set',
|
|
176
|
+
flex_mode: 'none',
|
|
177
|
+
columns: [{ tag: 'column', width: 'auto', elements: [dirButton(dir.name)] }],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (visibleDirCount === 0) {
|
|
182
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无子目录' } });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
elements.push({
|
|
186
|
+
tag: 'div',
|
|
187
|
+
text: { tag: 'lark_md', content: `(第 ${currentPage} 页无目录)` },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
elements.push({ tag: 'hr' });
|
|
191
|
+
elements.push({
|
|
192
|
+
tag: 'div',
|
|
193
|
+
text: { tag: 'lark_md', content: `**📄 文件 (${visibleFileCount})**` },
|
|
194
|
+
});
|
|
195
|
+
if (pageFiles.length > 0) {
|
|
196
|
+
for (const file of pageFiles) {
|
|
197
|
+
elements.push({
|
|
198
|
+
tag: 'column_set',
|
|
199
|
+
flex_mode: 'none',
|
|
200
|
+
columns: [
|
|
201
|
+
{ tag: 'column', width: 'auto', elements: [fileButton(file.name, file.size)] },
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (visibleFileCount === 0) {
|
|
207
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无文件' } });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
elements.push({
|
|
211
|
+
tag: 'div',
|
|
212
|
+
text: { tag: 'lark_md', content: `(第 ${currentPage} 页无文件)` },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (hasPagination) {
|
|
217
|
+
elements.push({ tag: 'hr' });
|
|
218
|
+
elements.push(...paginationBar({
|
|
219
|
+
cmd: 'ls.page',
|
|
220
|
+
offset: safeOffset,
|
|
221
|
+
pageSize: LS_PAGE_SIZE,
|
|
222
|
+
total: totalCount,
|
|
223
|
+
extra: { path: targetDir, root: browseRoot, ...(filter ? { q: filter } : {}) },
|
|
224
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
card: {
|
|
229
|
+
schema: '2.0',
|
|
230
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
231
|
+
header: {
|
|
232
|
+
title: { tag: 'plain_text', content: `📁 ${path.basename(targetDir)}` },
|
|
233
|
+
template: 'blue',
|
|
234
|
+
},
|
|
235
|
+
body: { elements },
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return { text: `读取目录失败: ${err.message}` };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=ls.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandContext, CommandResult } from '../../bridge/index.js';
|
|
2
|
+
import type { OrderEntry } from '../../order/index.js';
|
|
3
|
+
export declare const ORDER_PAGE_SIZE = 8;
|
|
4
|
+
export interface OrderCommandDeps {
|
|
5
|
+
reload(): void;
|
|
6
|
+
list(): OrderEntry[];
|
|
7
|
+
save(text: string): void;
|
|
8
|
+
updateText(id: string, text: string): void;
|
|
9
|
+
setAlias(id: string, alias: string | undefined): void;
|
|
10
|
+
resolveTarget(target: string): OrderEntry | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildOrderCommand(args: string[], ctx: CommandContext, offset: number, deps: OrderCommandDeps): CommandResult;
|