Skip to content

Commit 4b52dc3

Browse files
author
luozihao
committed
refactor(tui): dedupe repeated image paths in one submission
Referencing the same image path twice used to register two identical attachments (duplicate bytes, duplicate daemon upload). Rewrite now caches path -> placeholder per submit so repeats share one attachment, and the long replace callback is wrapped to the print-width convention.
1 parent 2056d16 commit 4b52dc3

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

apps/kimi-code/src/tui/utils/image-placeholder.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ const IMAGE_PATH_REGEX =
124124
const MAX_PATH_ATTACH_BYTES = 20 * 1024 * 1024;
125125
const MAX_PATH_ATTACHMENTS_PER_SUBMIT = 10;
126126

127-
function attachFromImagePath(raw: string, store: ImageAttachmentStore): string | undefined {
127+
function attachFromImagePath(
128+
raw: string,
129+
store: ImageAttachmentStore,
130+
seen: Map<string, string>,
131+
): string | undefined {
132+
const cached = seen.get(raw);
133+
if (cached !== undefined) return cached;
128134
const withoutQuotes = raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
129135
const candidates = [withoutQuotes];
130136
// A Windows path pasted into WSL has no native `D:\` backing here; try the
@@ -140,7 +146,9 @@ function attachFromImagePath(raw: string, store: ImageAttachmentStore): string |
140146
if (bytes.length === 0 || bytes.length > MAX_PATH_ATTACH_BYTES) continue;
141147
const meta = parseImageMeta(bytes);
142148
if (meta === null) continue;
143-
return `${store.addImage(bytes, meta.mime, meta.width, meta.height).placeholder} `;
149+
const placeholder = `${store.addImage(bytes, meta.mime, meta.width, meta.height).placeholder} `;
150+
seen.set(raw, placeholder);
151+
return placeholder;
144152
} catch {
145153
// unreadable / vanished between stat and read — next candidate
146154
}
@@ -150,9 +158,12 @@ function attachFromImagePath(raw: string, store: ImageAttachmentStore): string |
150158

151159
function rewriteImageFilePaths(text: string, store: ImageAttachmentStore): string {
152160
let conversions = 0;
153-
return text.replace(IMAGE_PATH_REGEX, (match, quoted: string | undefined, unquoted: string | undefined) => {
161+
// Repeated references to the same path share one attachment instead of
162+
// duplicating bytes and uploading twice.
163+
const seen = new Map<string, string>();
164+
return text.replace(IMAGE_PATH_REGEX, (match, quoted, unquoted) => {
154165
if (conversions >= MAX_PATH_ATTACHMENTS_PER_SUBMIT) return match;
155-
const placeholder = attachFromImagePath(quoted ?? unquoted ?? match, store);
166+
const placeholder = attachFromImagePath(quoted ?? unquoted ?? match, store, seen);
156167
if (placeholder === undefined) return match;
157168
conversions++;
158169
return placeholder;

apps/kimi-code/test/tui/input/image-placeholder.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,16 @@ describe('extractMediaAttachments: bare image paths', () => {
425425
expect(r.parts).toEqual([]);
426426
expect(text).toContain('D:\\微信聊天数据');
427427
});
428+
429+
it('reuses one attachment when the same path appears twice', () => {
430+
const dir = makeTempDir();
431+
const file = join(dir, 'dup.png');
432+
writeFileSync(file, PNG_1X1);
433+
const store = new ImageAttachmentStore();
434+
const r = extractMediaAttachments(`对比 ${file}${file} 的差异`, store);
435+
// Both spans resolve to the SAME stored attachment (dedupe) — two
436+
// references, one set of bytes; without dedupe this would be [1, 2].
437+
expect(r.imageAttachmentIds).toEqual([1, 1]);
438+
expect(r.parts.filter((p) => p.type === 'image_url')).toHaveLength(2);
439+
});
428440
});

0 commit comments

Comments
 (0)