@keyou007/agent-bot 0.1.14 → 0.1.15-alpha.1
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 +21 -0
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/help.js +6 -6
- package/dist/cli/initOptions.js +1 -4
- package/dist/cli/initOptions.js.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +91 -45
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +39 -12
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +108 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +24 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/feishu/InboundText.js +13 -0
- package/dist/feishu/InboundText.js.map +1 -0
- package/dist/feishu/MergedForwardMessage.js +317 -0
- package/dist/feishu/MergedForwardMessage.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +21 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +536 -58
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +161 -3
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli/help.ts +6 -6
- package/src/cli/initOptions.ts +1 -10
- package/src/cli.ts +10 -6
- package/src/feishu/CardRenderer.ts +116 -45
- package/src/feishu/FeishuConnector.ts +55 -13
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +151 -12
- package/src/feishu/FeishuTurnPresenter.ts +28 -4
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/feishu/InboundText.ts +12 -0
- package/src/feishu/MergedForwardMessage.ts +372 -0
- package/src/feishu/types.ts +15 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/presentation/OutboundRouter.ts +28 -0
- package/src/proxy/ProxySessionController.ts +642 -55
- package/src/state/StateStore.ts +230 -2
- package/src/state/migrations.ts +12 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- package/src/utils/executeShellCommand.ts +103 -15
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
ToolState,
|
|
12
12
|
} from "../runtime/types.js";
|
|
13
13
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
14
|
-
import { truncateText } from "../utils/markdown.js";
|
|
14
|
+
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
15
15
|
import { localCardImage } from "./LocalCardImage.js";
|
|
16
16
|
|
|
17
17
|
export interface StartupStatusView {
|
|
@@ -195,7 +195,6 @@ export interface DirectoryBrowserCardEntry {
|
|
|
195
195
|
name: string;
|
|
196
196
|
kind: "directory" | "drive" | "file" | "image" | "binary";
|
|
197
197
|
openAction?: TaskListCardAction;
|
|
198
|
-
actions?: TaskListCardAction[];
|
|
199
198
|
}
|
|
200
199
|
|
|
201
200
|
export interface DirectoryBrowserCardView {
|
|
@@ -233,6 +232,17 @@ export interface DismissGroupCardView {
|
|
|
233
232
|
requestedBy: string;
|
|
234
233
|
}
|
|
235
234
|
|
|
235
|
+
export interface ShellCommandCardView {
|
|
236
|
+
command: string;
|
|
237
|
+
cwd: string;
|
|
238
|
+
stdout: string;
|
|
239
|
+
stderr: string;
|
|
240
|
+
status: "running" | "completed" | "failed" | "timed_out";
|
|
241
|
+
exitCode?: number | null;
|
|
242
|
+
elapsedMs: number;
|
|
243
|
+
outputTruncated: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
export class CardRenderer {
|
|
237
247
|
private readonly thinkingCardLayout: ThinkingCardLayout;
|
|
238
248
|
|
|
@@ -240,6 +250,35 @@ export class CardRenderer {
|
|
|
240
250
|
this.thinkingCardLayout = options.thinkingCardLayout ?? "grouped";
|
|
241
251
|
}
|
|
242
252
|
|
|
253
|
+
renderShellCommandCard(view: ShellCommandCardView): Record<string, unknown> {
|
|
254
|
+
const stdout = normalizeTerminalOutput(view.stdout).trimEnd();
|
|
255
|
+
const stderr = normalizeTerminalOutput(view.stderr).trimEnd();
|
|
256
|
+
const rawOutput = [
|
|
257
|
+
stdout,
|
|
258
|
+
stderr ? `[stderr]\n${stderr}` : "",
|
|
259
|
+
].filter(Boolean).join("\n");
|
|
260
|
+
const displayOutput = truncateMiddle(
|
|
261
|
+
rawOutput,
|
|
262
|
+
6_000,
|
|
263
|
+
"\n\n... 中间输出已截断 ...\n\n",
|
|
264
|
+
);
|
|
265
|
+
const command = truncateMiddle(view.command.trim(), 600, " ... ");
|
|
266
|
+
const output = displayOutput || (view.status === "running" ? "等待输出..." : "(无输出)");
|
|
267
|
+
const status = shellCommandStatus(view);
|
|
268
|
+
const outputWasTruncated = view.outputTruncated || rawOutput.length > 6_000;
|
|
269
|
+
return sectionCard(
|
|
270
|
+
status.title,
|
|
271
|
+
[
|
|
272
|
+
markdown(codeBlock(`$ ${command}\n${output}`, 6_700)),
|
|
273
|
+
markdown([
|
|
274
|
+
`${inlineCode(view.cwd)} · ${status.label} · ${formatTurnDuration(view.elapsedMs)}`,
|
|
275
|
+
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
276
|
+
].join("\n")),
|
|
277
|
+
],
|
|
278
|
+
status.template,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
243
282
|
renderExecutionSettings(view: ExecutionSettingsCardView): Record<string, unknown> {
|
|
244
283
|
const baseAction: Record<string, string> = {
|
|
245
284
|
contextKey: view.contextKey,
|
|
@@ -803,7 +842,6 @@ export class CardRenderer {
|
|
|
803
842
|
schema: "2.0",
|
|
804
843
|
config: {
|
|
805
844
|
update_multi: true,
|
|
806
|
-
width_mode: "fill",
|
|
807
845
|
},
|
|
808
846
|
header: {
|
|
809
847
|
template: view.kind === "first" ? "turquoise" : view.kind === "upgrade" ? "blue" : "green",
|
|
@@ -869,7 +907,7 @@ export class CardRenderer {
|
|
|
869
907
|
});
|
|
870
908
|
elements.push(
|
|
871
909
|
{ tag: "hr" },
|
|
872
|
-
taskActionRow(footerActions),
|
|
910
|
+
taskActionRow(footerActions, renderTurnDuration(state)),
|
|
873
911
|
);
|
|
874
912
|
} else if (state.status === "completed") {
|
|
875
913
|
footerActions.push({
|
|
@@ -880,6 +918,11 @@ export class CardRenderer {
|
|
|
880
918
|
{ tag: "hr" },
|
|
881
919
|
taskActionRow(footerActions),
|
|
882
920
|
);
|
|
921
|
+
} else if (state.status === "starting") {
|
|
922
|
+
elements.push(
|
|
923
|
+
{ tag: "hr" },
|
|
924
|
+
taskActionRow([], renderTurnDuration(state)),
|
|
925
|
+
);
|
|
883
926
|
}
|
|
884
927
|
if (state.status === "completed") {
|
|
885
928
|
elements.push(
|
|
@@ -1724,37 +1767,27 @@ function groupedLiveActivityPage(
|
|
|
1724
1767
|
const commentaryIndexes = groups
|
|
1725
1768
|
.map((group, index) => isCommentaryGroup(group) ? index : -1)
|
|
1726
1769
|
.filter((index) => index >= 0);
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const renderSelection = (selection: ReadonlySet<number>): LiveGroupedActivityPage => ({
|
|
1738
|
-
groups: groupsWithGaps(groups, selection),
|
|
1739
|
-
historyGroups: groups.filter((_group, index) => index < latestCommentary || !selection.has(index)),
|
|
1740
|
-
});
|
|
1741
|
-
const fits = (selection: ReadonlySet<number>): boolean => {
|
|
1742
|
-
const view = renderSelection(selection);
|
|
1743
|
-
return groupedActivityPageFits(view.groups, projectCwd, {});
|
|
1770
|
+
const renderCutoff = (cutoff: number): LiveGroupedActivityPage => {
|
|
1771
|
+
const selected = new Set<number>(commentaryIndexes
|
|
1772
|
+
.filter((index) => index < cutoff)
|
|
1773
|
+
.slice(-PINNED_LIVE_COMMENTARIES));
|
|
1774
|
+
for (let index = cutoff; index < groups.length; index += 1) selected.add(index);
|
|
1775
|
+
return {
|
|
1776
|
+
groups: groupsWithGaps(groups, selected),
|
|
1777
|
+
historyGroups: groups.slice(0, cutoff),
|
|
1778
|
+
};
|
|
1744
1779
|
};
|
|
1745
1780
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
if (fits(selected) || index === newestExecution) break;
|
|
1753
|
-
selected.delete(index);
|
|
1754
|
-
}
|
|
1781
|
+
const executionCutoffs = groups
|
|
1782
|
+
.map((group, index) => group.kind === "execution" && group.tools.length > 0 ? index : -1)
|
|
1783
|
+
.filter((index) => index > 0);
|
|
1784
|
+
for (const cutoff of executionCutoffs) {
|
|
1785
|
+
const view = renderCutoff(cutoff);
|
|
1786
|
+
if (groupedActivityPageFits(view.groups, projectCwd, {})) return view;
|
|
1755
1787
|
}
|
|
1756
1788
|
|
|
1757
|
-
return
|
|
1789
|
+
if (executionCutoffs.length > 0) return renderCutoff(executionCutoffs.at(-1)!);
|
|
1790
|
+
return { groups: latest, historyGroups: groups.slice(0, latestStart) };
|
|
1758
1791
|
}
|
|
1759
1792
|
|
|
1760
1793
|
function isCommentaryGroup(group: GroupedTurnActivity): boolean {
|
|
@@ -1964,10 +1997,26 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
1964
1997
|
? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
|
|
1965
1998
|
: normalizedCommand;
|
|
1966
1999
|
const commandText = truncateText(displayCommand, 600);
|
|
1967
|
-
const resultText = result ? truncateToolResult(
|
|
2000
|
+
const resultText = result ? truncateToolResult(normalizeTerminalOutput(result).trim()) : undefined;
|
|
1968
2001
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
1969
2002
|
}
|
|
1970
2003
|
|
|
2004
|
+
function normalizeTerminalOutput(value: string): string {
|
|
2005
|
+
const clean = stripAnsi(value).replace(/\r\n/g, "\n");
|
|
2006
|
+
return clean
|
|
2007
|
+
.split("\n")
|
|
2008
|
+
.map((line) => {
|
|
2009
|
+
if (!line.includes("\r")) return line;
|
|
2010
|
+
const updates = line.split("\r");
|
|
2011
|
+
for (let index = updates.length - 1; index >= 0; index -= 1) {
|
|
2012
|
+
const update = updates[index];
|
|
2013
|
+
if (update) return update;
|
|
2014
|
+
}
|
|
2015
|
+
return "";
|
|
2016
|
+
})
|
|
2017
|
+
.join("\n");
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1971
2020
|
function truncateToolResult(result: string): string {
|
|
1972
2021
|
const maxLength = 900;
|
|
1973
2022
|
const marker = "\n...\n";
|
|
@@ -2054,6 +2103,23 @@ function formatTurnDuration(durationMs: number): string {
|
|
|
2054
2103
|
return `${seconds}s`;
|
|
2055
2104
|
}
|
|
2056
2105
|
|
|
2106
|
+
function shellCommandStatus(view: ShellCommandCardView): {
|
|
2107
|
+
title: string;
|
|
2108
|
+
label: string;
|
|
2109
|
+
template: string;
|
|
2110
|
+
} {
|
|
2111
|
+
if (view.status === "running") {
|
|
2112
|
+
return { title: "正在执行命令", label: "运行中", template: "blue" };
|
|
2113
|
+
}
|
|
2114
|
+
if (view.status === "timed_out") {
|
|
2115
|
+
return { title: "命令执行超时", label: "已超时(120s)", template: "orange" };
|
|
2116
|
+
}
|
|
2117
|
+
if (view.status === "completed") {
|
|
2118
|
+
return { title: "命令执行完成", label: `退出码 ${view.exitCode ?? 0}`, template: "green" };
|
|
2119
|
+
}
|
|
2120
|
+
return { title: "命令执行失败", label: `退出码 ${view.exitCode ?? "未知"}`, template: "red" };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2057
2123
|
function unwrapShellCommand(command: string): string | undefined {
|
|
2058
2124
|
return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
|
|
2059
2125
|
}
|
|
@@ -2415,18 +2481,29 @@ function resetHistoryEntryRow(
|
|
|
2415
2481
|
};
|
|
2416
2482
|
}
|
|
2417
2483
|
|
|
2418
|
-
function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
|
|
2484
|
+
function taskActionRow(actions: TaskListCardAction[], trailingText?: string): Record<string, unknown> {
|
|
2485
|
+
const columns = actions.map((action) => ({
|
|
2486
|
+
tag: "column",
|
|
2487
|
+
width: "auto",
|
|
2488
|
+
vertical_align: "center",
|
|
2489
|
+
elements: [taskActionElement(action)],
|
|
2490
|
+
}));
|
|
2491
|
+
if (trailingText) {
|
|
2492
|
+
columns.push({
|
|
2493
|
+
tag: "column",
|
|
2494
|
+
width: "auto",
|
|
2495
|
+
vertical_align: "center",
|
|
2496
|
+
elements: [markdown(
|
|
2497
|
+
`<font color='grey'>${actions.length > 0 ? "· " : ""}${escapeCardHtml(trailingText)}</font>`,
|
|
2498
|
+
)],
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2419
2501
|
return {
|
|
2420
2502
|
tag: "column_set",
|
|
2421
2503
|
flex_mode: "flow",
|
|
2422
2504
|
horizontal_spacing: "8px",
|
|
2423
2505
|
margin: "2px 0 0 0",
|
|
2424
|
-
columns
|
|
2425
|
-
tag: "column",
|
|
2426
|
-
width: "auto",
|
|
2427
|
-
vertical_align: "center",
|
|
2428
|
-
elements: [taskActionElement(action)],
|
|
2429
|
-
})),
|
|
2506
|
+
columns,
|
|
2430
2507
|
};
|
|
2431
2508
|
}
|
|
2432
2509
|
|
|
@@ -2448,12 +2525,6 @@ function directoryBrowserEntryRow(entry: DirectoryBrowserCardEntry): Record<stri
|
|
|
2448
2525
|
? [taskActionElement({ ...entry.openAction, text: label })]
|
|
2449
2526
|
: [markdown(escapeCardHtml(label))],
|
|
2450
2527
|
},
|
|
2451
|
-
...(entry.actions ?? []).map((action) => ({
|
|
2452
|
-
tag: "column",
|
|
2453
|
-
width: "auto",
|
|
2454
|
-
vertical_align: "center",
|
|
2455
|
-
elements: [taskActionElement(action)],
|
|
2456
|
-
})),
|
|
2457
2528
|
],
|
|
2458
2529
|
};
|
|
2459
2530
|
}
|
|
@@ -2,8 +2,9 @@ import type { Logger } from "pino";
|
|
|
2
2
|
import type { AppConfig } from "../config/schema.js";
|
|
3
3
|
import { threadContextKey } from "./contextKey.js";
|
|
4
4
|
import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
|
|
5
|
+
import { normalizeFeishuPostText } from "./InboundText.js";
|
|
5
6
|
import { allowsFeishuUser } from "./ownerAccess.js";
|
|
6
|
-
import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
7
|
+
import type { CardAction, ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
7
8
|
|
|
8
9
|
type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
|
|
9
10
|
type BotOpenIdListener = (botOpenId: string) => void;
|
|
@@ -88,6 +89,7 @@ export class FeishuConnector {
|
|
|
88
89
|
});
|
|
89
90
|
},
|
|
90
91
|
"card.action.trigger": async (data: unknown) => {
|
|
92
|
+
const receivedAt = Date.now();
|
|
91
93
|
const operatorOpenId = getFeishuEvent(data)?.operator?.open_id;
|
|
92
94
|
if (!allowsFeishuUser(this.config, operatorOpenId)) {
|
|
93
95
|
this.logger.debug(
|
|
@@ -102,11 +104,11 @@ export class FeishuConnector {
|
|
|
102
104
|
return {};
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
this.deferCardAction(action, receivedAt);
|
|
108
|
+
this.logger.debug(
|
|
109
|
+
{ actionId: action.actionId, messageId: action.messageId, responsePreparedInMs: Date.now() - receivedAt },
|
|
110
|
+
"Prepared the Feishu card action acknowledgement.",
|
|
111
|
+
);
|
|
110
112
|
return {
|
|
111
113
|
toast: {
|
|
112
114
|
type: "success",
|
|
@@ -122,6 +124,27 @@ export class FeishuConnector {
|
|
|
122
124
|
await wsClient.start({ eventDispatcher });
|
|
123
125
|
this.logger.info("Feishu WebSocket connector started.");
|
|
124
126
|
}
|
|
127
|
+
|
|
128
|
+
private deferCardAction(action: CardAction, receivedAt: number): void {
|
|
129
|
+
setImmediate(() => {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
this.logger.debug(
|
|
132
|
+
{ actionId: action.actionId, messageId: action.messageId, dispatchDelayMs: startedAt - receivedAt },
|
|
133
|
+
"Started deferred Feishu card action handling.",
|
|
134
|
+
);
|
|
135
|
+
void Promise.resolve()
|
|
136
|
+
.then(() => this.handler.onCardAction(action))
|
|
137
|
+
.then(() => {
|
|
138
|
+
this.logger.debug(
|
|
139
|
+
{ actionId: action.actionId, messageId: action.messageId, durationMs: Date.now() - startedAt },
|
|
140
|
+
"Completed Feishu card action handling.",
|
|
141
|
+
);
|
|
142
|
+
})
|
|
143
|
+
.catch((error: unknown) => {
|
|
144
|
+
this.logger.error({ error, actionId: action.actionId }, "Failed to handle Feishu card action.");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
|
|
@@ -171,8 +194,9 @@ function toIncomingMessage(
|
|
|
171
194
|
return undefined;
|
|
172
195
|
}
|
|
173
196
|
|
|
197
|
+
const messageId = message.message_id ?? `${Date.now()}`;
|
|
174
198
|
return {
|
|
175
|
-
messageId
|
|
199
|
+
messageId,
|
|
176
200
|
contextKey: chatId
|
|
177
201
|
? threadContext
|
|
178
202
|
? threadContextKey(chatId, threadId!)
|
|
@@ -189,6 +213,8 @@ function toIncomingMessage(
|
|
|
189
213
|
...(mentionedBot ? { mentionedBot: true as const } : {}),
|
|
190
214
|
text: parsed.text,
|
|
191
215
|
...(parsed.images.length > 0 ? { images: parsed.images.map((imageKey) => ({ imageKey })) } : {}),
|
|
216
|
+
...(parsed.files.length > 0 ? { files: parsed.files } : {}),
|
|
217
|
+
...(parsed.mergedForward ? { mergedForwardMessageId: messageId } : {}),
|
|
192
218
|
};
|
|
193
219
|
}
|
|
194
220
|
|
|
@@ -201,21 +227,36 @@ function parseMessageContent(
|
|
|
201
227
|
messageType: unknown,
|
|
202
228
|
content: Record<string, unknown>,
|
|
203
229
|
mentions: unknown,
|
|
204
|
-
): {
|
|
230
|
+
): {
|
|
231
|
+
text: string;
|
|
232
|
+
images: string[];
|
|
233
|
+
files: Array<{ fileKey: string; fileName: string }>;
|
|
234
|
+
mergedForward?: true;
|
|
235
|
+
} | undefined {
|
|
205
236
|
if (messageType === "text") {
|
|
206
237
|
const rawText = typeof content.text === "string" ? content.text : "";
|
|
207
|
-
return { text: stripLeadingMentions(rawText, mentions), images: [] };
|
|
238
|
+
return { text: normalizeFeishuPostText(stripLeadingMentions(rawText, mentions)), images: [], files: [] };
|
|
208
239
|
}
|
|
209
240
|
if (messageType === "image") {
|
|
210
241
|
const imageKey = typeof content.image_key === "string" ? content.image_key : undefined;
|
|
211
|
-
return imageKey ? { text: "", images: [imageKey] } : undefined;
|
|
242
|
+
return imageKey ? { text: "", images: [imageKey], files: [] } : undefined;
|
|
243
|
+
}
|
|
244
|
+
if (messageType === "file") {
|
|
245
|
+
const fileKey = typeof content.file_key === "string" ? content.file_key : undefined;
|
|
246
|
+
const fileName = typeof content.file_name === "string" && content.file_name.trim()
|
|
247
|
+
? content.file_name.trim()
|
|
248
|
+
: "file";
|
|
249
|
+
return fileKey ? { text: "", images: [], files: [{ fileKey, fileName }] } : undefined;
|
|
250
|
+
}
|
|
251
|
+
if (messageType === "merge_forward") {
|
|
252
|
+
return { text: "", images: [], files: [], mergedForward: true };
|
|
212
253
|
}
|
|
213
254
|
if (messageType !== "post") return undefined;
|
|
214
255
|
|
|
215
256
|
const locale = selectPostLocale(content);
|
|
216
257
|
if (!locale) return undefined;
|
|
217
258
|
const paragraphs: string[] = [];
|
|
218
|
-
const title = typeof locale.title === "string" ? locale.title
|
|
259
|
+
const title = typeof locale.title === "string" ? normalizeFeishuPostText(locale.title) : "";
|
|
219
260
|
if (title) paragraphs.push(title);
|
|
220
261
|
const images: string[] = [];
|
|
221
262
|
if (Array.isArray(locale.content)) {
|
|
@@ -230,10 +271,11 @@ function parseMessageContent(
|
|
|
230
271
|
images.push(element.image_key);
|
|
231
272
|
}
|
|
232
273
|
}
|
|
233
|
-
|
|
274
|
+
const normalizedRow = normalizeFeishuPostText(rowText);
|
|
275
|
+
if (normalizedRow) paragraphs.push(normalizedRow);
|
|
234
276
|
}
|
|
235
277
|
}
|
|
236
|
-
return { text: paragraphs.join("\n"), images: [...new Set(images)] };
|
|
278
|
+
return { text: paragraphs.join("\n"), images: [...new Set(images)], files: [] };
|
|
237
279
|
}
|
|
238
280
|
|
|
239
281
|
function selectPostLocale(content: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
1
3
|
interface FenceState {
|
|
2
4
|
indent: string;
|
|
3
5
|
marker: "`" | "~";
|
|
@@ -9,7 +11,7 @@ const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
|
|
|
9
11
|
const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<?[^)\r\n]+>?)\)/g;
|
|
10
12
|
const LOCAL_FILE_LINE_REFERENCE = /(:\d+(?::\d+)?)$/;
|
|
11
13
|
|
|
12
|
-
export function normalizeFeishuMarkdown(markdown: string): string {
|
|
14
|
+
export function normalizeFeishuMarkdown(markdown: string, projectCwd?: string): string {
|
|
13
15
|
const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
|
|
14
16
|
let fence: FenceState | undefined;
|
|
15
17
|
|
|
@@ -19,7 +21,7 @@ export function normalizeFeishuMarkdown(markdown: string): string {
|
|
|
19
21
|
|
|
20
22
|
if (!fence) {
|
|
21
23
|
const opener = FENCE_OPENER.exec(body);
|
|
22
|
-
if (!opener?.[2]) return `${includeLocalFileLineReferences(body)}${ending}`;
|
|
24
|
+
if (!opener?.[2]) return `${includeLocalFileLineReferences(body, projectCwd)}${ending}`;
|
|
23
25
|
|
|
24
26
|
const indent = opener[1] ?? "";
|
|
25
27
|
const delimiter = opener[2];
|
|
@@ -40,7 +42,7 @@ export function normalizeFeishuMarkdown(markdown: string): string {
|
|
|
40
42
|
}).join("");
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function includeLocalFileLineReferences(markdown: string): string {
|
|
45
|
+
function includeLocalFileLineReferences(markdown: string, projectCwd?: string): string {
|
|
44
46
|
return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
|
|
45
47
|
if (imageMarker) return link;
|
|
46
48
|
|
|
@@ -48,11 +50,68 @@ function includeLocalFileLineReferences(markdown: string): string {
|
|
|
48
50
|
if (!isLocalFileTarget(target)) return link;
|
|
49
51
|
|
|
50
52
|
const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const projectPathLabel = projectCwd ? projectFilePathLabel(target, projectCwd, reference) : undefined;
|
|
54
|
+
const normalizedLabel = projectPathLabel
|
|
55
|
+
?? (reference && !label.endsWith(reference) ? `${label}${reference}` : label);
|
|
56
|
+
return normalizedLabel === label ? link : `[${normalizedLabel}](${rawTarget})`;
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
function projectFilePathLabel(
|
|
61
|
+
target: string,
|
|
62
|
+
projectCwd: string,
|
|
63
|
+
reference: string | undefined,
|
|
64
|
+
): string | undefined {
|
|
65
|
+
const targetWithoutReference = reference ? target.slice(0, -reference.length) : target;
|
|
66
|
+
const filePath = localFilePath(targetWithoutReference);
|
|
67
|
+
if (!filePath) return undefined;
|
|
68
|
+
if (!isInsideProject(filePath, projectCwd)) {
|
|
69
|
+
return `${displayAbsolutePath(filePath)}${reference ?? ""}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pathApi = usesWindowsPaths(filePath, projectCwd) ? path.win32 : path.posix;
|
|
73
|
+
const parsed = pathApi.parse(filePath);
|
|
74
|
+
if (Array.from(parsed.name).length > 5) return undefined;
|
|
75
|
+
|
|
76
|
+
const parentName = pathApi.basename(pathApi.dirname(filePath));
|
|
77
|
+
if (!parentName) return undefined;
|
|
78
|
+
return `${parentName}${pathApi.sep}${parsed.base}${reference ?? ""}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function localFilePath(target: string): string | undefined {
|
|
82
|
+
if (!/^file:\/\//i.test(target)) return normalizeWindowsDrivePrefix(target);
|
|
83
|
+
|
|
84
|
+
let value = target.slice("file://".length);
|
|
85
|
+
try {
|
|
86
|
+
value = decodeURIComponent(value);
|
|
87
|
+
} catch {
|
|
88
|
+
// Keep malformed percent escapes visible instead of dropping the link.
|
|
89
|
+
}
|
|
90
|
+
if (/^\/[a-z]:[\\/]/i.test(value)) return value.slice(1);
|
|
91
|
+
if (value.startsWith("/")) return value;
|
|
92
|
+
return `//${value}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeWindowsDrivePrefix(value: string): string {
|
|
96
|
+
return /^\/[a-z]:[\\/]/i.test(value) ? value.slice(1) : value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isInsideProject(filePath: string, projectCwd: string): boolean {
|
|
100
|
+
const pathApi = usesWindowsPaths(filePath, projectCwd) ? path.win32 : path.posix;
|
|
101
|
+
const relative = pathApi.relative(pathApi.resolve(projectCwd), pathApi.resolve(filePath));
|
|
102
|
+
return relative === ""
|
|
103
|
+
|| (relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function displayAbsolutePath(filePath: string): string {
|
|
107
|
+
if (!usesWindowsPaths(filePath)) return path.posix.normalize(filePath);
|
|
108
|
+
return path.win32.normalize(filePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function usesWindowsPaths(...values: string[]): boolean {
|
|
112
|
+
return values.some((value) => /^\/?[a-z]:[\\/]/i.test(value) || /^\\\\/.test(value));
|
|
113
|
+
}
|
|
114
|
+
|
|
56
115
|
function unwrapMarkdownTarget(target: string): string {
|
|
57
116
|
return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
|
|
58
117
|
}
|