dsh-ntr 0.3.1 → 0.3.3
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/README.en.md +3 -1
- package/README.md +3 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +2436 -82
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +149 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +279 -16
- package/lib/client/model-seat.js +8 -2
- package/lib/client/panel.js +96 -29
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +380 -13
- package/lib/client.js +1364 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1229 -8
- package/lib/index.js +34 -0
- package/lib/protocol.js +19 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +850 -18
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +47 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/model-seat.d.ts +21 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +108 -1
- package/lib/types/impl.d.ts +94 -7
- package/lib/types/protocol.d.ts +145 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +2 -1
- package/src/bridge.ts +2754 -86
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +178 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +298 -15
- package/src/client/model-seat.tsx +28 -3
- package/src/client/panel.tsx +157 -40
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +412 -14
- package/src/impl.ts +1251 -13
- package/src/index.ts +33 -0
- package/src/protocol.ts +145 -0
- package/src/tps.ts +40 -0
package/src/impl.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from './ids.ts'
|
|
9
9
|
import type {
|
|
10
|
-
AcpAgentCatalog, AcpAgentDef, AcpCatalogOption, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult,
|
|
10
|
+
AcpAgentCatalog, AcpAgentDef, AcpCatalogOption, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpIdleRelease, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult,
|
|
11
11
|
AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult,
|
|
12
12
|
} from './protocol.ts'
|
|
13
|
-
import { DEFAULT_TAG_HALO } from './protocol.ts'
|
|
13
|
+
import { DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_TAG_HALO, IDLE_RELEASE_MS } from './protocol.ts'
|
|
14
14
|
|
|
15
15
|
export { randomUUID }
|
|
16
16
|
|
|
@@ -175,6 +175,21 @@ export function validateApprovalDeadlines(value: unknown): Partial<Record<Approv
|
|
|
175
175
|
return out
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Validate the idle-release window the settings page writes.
|
|
180
|
+
*
|
|
181
|
+
* The window is expressed in MILLISECONDS on the shared ladder
|
|
182
|
+
* ({@link IDLE_RELEASE_MS}), so a value the slider can produce is always a
|
|
183
|
+
* value the host runs — the same one-ladder rule the approval deadline card
|
|
184
|
+
* follows.
|
|
185
|
+
*/
|
|
186
|
+
export function validateIdleReleaseMs(raw: unknown, code: 'bad_config' | 'bad_payload'): number {
|
|
187
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || !IDLE_RELEASE_MS.includes(raw)) {
|
|
188
|
+
throw new AcpError(code, `acp: idleTimeoutMs must be one of ${IDLE_RELEASE_MS.join(', ')}`)
|
|
189
|
+
}
|
|
190
|
+
return raw
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
/**
|
|
179
194
|
* Validate the mode a write asks for. Three values, and the list is the whole
|
|
180
195
|
* vocabulary: an unanswered ask is either answered for you, kept waiting, or
|
|
@@ -523,6 +538,14 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
523
538
|
// `abort` a zero window would refuse every ask on arrival — the agent stops
|
|
524
539
|
// at every gate — so it becomes the shortest real wait.
|
|
525
540
|
const effectivePermissionTimeoutMs = normalizeApprovalWindow(permissionTimeoutMode, permissionTimeoutMs)
|
|
541
|
+
// The chat-process idle-release window (the settings page edits it).
|
|
542
|
+
// Same shape as the per-agent field: 0 = never release, otherwise an
|
|
543
|
+
// integer number of milliseconds.
|
|
544
|
+
const idleTimeoutMs = value.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
545
|
+
if (typeof idleTimeoutMs !== 'number' || !Number.isInteger(idleTimeoutMs)
|
|
546
|
+
|| idleTimeoutMs > 86_400_000 || (idleTimeoutMs !== 0 && idleTimeoutMs < 1_000)) {
|
|
547
|
+
throw new AcpError('bad_config', 'acp: idleTimeoutMs must be 0 (never release) or an integer in [1000, 86400000]')
|
|
548
|
+
}
|
|
526
549
|
const cancelFallbackMs = value.cancelFallbackMs ?? 15_000
|
|
527
550
|
if (typeof cancelFallbackMs !== 'number' || !Number.isInteger(cancelFallbackMs) || cancelFallbackMs < 0 || cancelFallbackMs > 600_000) {
|
|
528
551
|
throw new AcpError('bad_config', 'acp: cancelFallbackMs must be an integer in [0, 600000] (0 = disable the fallback)')
|
|
@@ -564,6 +587,7 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
564
587
|
turnStallMs,
|
|
565
588
|
turnStallToolMs,
|
|
566
589
|
turnStallTickMs,
|
|
590
|
+
idleTimeoutMs,
|
|
567
591
|
}
|
|
568
592
|
}
|
|
569
593
|
|
|
@@ -1363,8 +1387,1053 @@ export function describeUnhandledUpdate(kind: string, update: Record<string, unk
|
|
|
1363
1387
|
* injected context too (goal/subagent/skill material), and that turn carries no
|
|
1364
1388
|
* user intent to send anywhere.
|
|
1365
1389
|
*/
|
|
1390
|
+
export interface PromptImageContent {
|
|
1391
|
+
type: 'image'
|
|
1392
|
+
data: string
|
|
1393
|
+
mimeType: string
|
|
1394
|
+
uri?: string
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
export interface PromptTextContent {
|
|
1398
|
+
type: 'text'
|
|
1399
|
+
text: string
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
export type PromptContentBlock = PromptTextContent | PromptImageContent
|
|
1403
|
+
|
|
1404
|
+
/** Helper: guess MIME type from file extension or URL */
|
|
1405
|
+
export function guessImageMimeType(pathOrUrl?: string): string | undefined {
|
|
1406
|
+
if (!pathOrUrl) return undefined
|
|
1407
|
+
const clean = pathOrUrl.split('?')[0].split('#')[0].toLowerCase()
|
|
1408
|
+
if (clean.endsWith('.jpg') || clean.endsWith('.jpeg')) return 'image/jpeg'
|
|
1409
|
+
if (clean.endsWith('.png')) return 'image/png'
|
|
1410
|
+
if (clean.endsWith('.gif')) return 'image/gif'
|
|
1411
|
+
if (clean.endsWith('.webp')) return 'image/webp'
|
|
1412
|
+
if (clean.endsWith('.svg') || clean.endsWith('.svgz')) return 'image/svg+xml'
|
|
1413
|
+
if (clean.endsWith('.bmp')) return 'image/bmp'
|
|
1414
|
+
if (clean.endsWith('.ico')) return 'image/x-icon'
|
|
1415
|
+
if (clean.endsWith('.tiff') || clean.endsWith('.tif')) return 'image/tiff'
|
|
1416
|
+
if (clean.endsWith('.avif')) return 'image/avif'
|
|
1417
|
+
if (clean.endsWith('.heic')) return 'image/heic'
|
|
1418
|
+
if (clean.endsWith('.heif')) return 'image/heif'
|
|
1419
|
+
if (clean.endsWith('.apng')) return 'image/apng'
|
|
1420
|
+
return undefined
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** Helper: parse Data URI */
|
|
1424
|
+
export function parseImageDataUrl(url: string): { data: string; mimeType: string } | undefined {
|
|
1425
|
+
if (!url.startsWith('data:')) return undefined
|
|
1426
|
+
const commaIdx = url.indexOf(',')
|
|
1427
|
+
if (commaIdx === -1) return undefined
|
|
1428
|
+
const header = url.slice(5, commaIdx)
|
|
1429
|
+
const raw = url.slice(commaIdx + 1).trim()
|
|
1430
|
+
const parts = header.split(';')
|
|
1431
|
+
const mimeType = parts[0] && parts[0].trim() !== '' ? parts[0].trim().toLowerCase() : 'image/png'
|
|
1432
|
+
if (!mimeType.startsWith('image/')) return undefined
|
|
1433
|
+
const isBase64 = parts.some(p => p.trim() === 'base64')
|
|
1434
|
+
let data: string
|
|
1435
|
+
if (isBase64) {
|
|
1436
|
+
data = raw.replace(/\s+/g, '')
|
|
1437
|
+
} else {
|
|
1438
|
+
try {
|
|
1439
|
+
const decoded = decodeURIComponent(raw)
|
|
1440
|
+
data = typeof Buffer !== 'undefined' ? Buffer.from(decoded).toString('base64') : btoa(decoded)
|
|
1441
|
+
} catch {
|
|
1442
|
+
data = typeof Buffer !== 'undefined' ? Buffer.from(raw).toString('base64') : btoa(raw)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return { mimeType, data }
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
export function isLocalFilePath(str: string): boolean {
|
|
1449
|
+
if (typeof str !== 'string' || str === '') return false
|
|
1450
|
+
return isAbsolutePath(str) || str.startsWith('./') || str.startsWith('.\\') || str.startsWith('../') || str.startsWith('..\\')
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export function normalizeFileUrlToPath(uri: string): string {
|
|
1454
|
+
if (!uri.startsWith('file://')) return uri
|
|
1455
|
+
let raw = uri.slice('file://'.length)
|
|
1456
|
+
try {
|
|
1457
|
+
raw = decodeURIComponent(raw)
|
|
1458
|
+
} catch {}
|
|
1459
|
+
if (raw.startsWith('localhost/')) {
|
|
1460
|
+
raw = raw.slice('localhost'.length)
|
|
1461
|
+
}
|
|
1462
|
+
if (/^\/?[a-zA-Z]:/.test(raw)) {
|
|
1463
|
+
return raw.startsWith('/') ? raw.slice(1) : raw
|
|
1464
|
+
}
|
|
1465
|
+
if (raw.startsWith('/')) {
|
|
1466
|
+
return raw
|
|
1467
|
+
}
|
|
1468
|
+
return `//${raw}`
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function expandHomePath(p: string): string {
|
|
1472
|
+
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
|
|
1473
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
1474
|
+
const env = process.env
|
|
1475
|
+
const home = env.HOME || env.USERPROFILE || (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined)
|
|
1476
|
+
if (home) {
|
|
1477
|
+
const cleanHome = home.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
1478
|
+
return p === '~' ? cleanHome : `${cleanHome}/${p.slice(2).replaceAll('\\', '/')}`
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return p
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function readImageFileFromContext(filePath: string, context?: unknown): Buffer | Uint8Array | string | undefined {
|
|
1486
|
+
const target = normalizeFileUrlToPath(filePath)
|
|
1487
|
+
const expanded = expandHomePath(target)
|
|
1488
|
+
const pathsToTry = target === expanded ? [target] : [target, expanded]
|
|
1489
|
+
|
|
1490
|
+
const builtinFs = typeof process !== 'undefined' && typeof (process as unknown as { getBuiltinModule?: (m: string) => unknown }).getBuiltinModule === 'function'
|
|
1491
|
+
? ((process as unknown as { getBuiltinModule: (m: string) => unknown }).getBuiltinModule('fs') as { existsSync?: (p: string) => boolean; readFileSync?: (p: string) => Buffer } | null)
|
|
1492
|
+
: null
|
|
1493
|
+
|
|
1494
|
+
for (const p of pathsToTry) {
|
|
1495
|
+
if (context && typeof (context as { readFile?: unknown }).readFile === 'function') {
|
|
1496
|
+
try {
|
|
1497
|
+
const res = (context as { readFile: (path: string) => unknown }).readFile(p)
|
|
1498
|
+
if (res !== undefined) return res as Buffer | Uint8Array | string
|
|
1499
|
+
} catch {}
|
|
1500
|
+
}
|
|
1501
|
+
if (context && typeof (context as { fs?: { readFileSync?: unknown } }).fs?.readFileSync === 'function') {
|
|
1502
|
+
try {
|
|
1503
|
+
const res = (context as { fs: { readFileSync: (path: string) => unknown } }).fs.readFileSync(p)
|
|
1504
|
+
if (res !== undefined) return res as Buffer | Uint8Array | string
|
|
1505
|
+
} catch {}
|
|
1506
|
+
}
|
|
1507
|
+
if (builtinFs && typeof builtinFs.existsSync === 'function' && typeof builtinFs.readFileSync === 'function') {
|
|
1508
|
+
try {
|
|
1509
|
+
if (builtinFs.existsSync(p)) return builtinFs.readFileSync(p)
|
|
1510
|
+
} catch {}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return undefined
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Detect image MIME type by examining binary magic numbers.
|
|
1518
|
+
* Supports Buffer, Uint8Array, and base64 strings.
|
|
1519
|
+
*/
|
|
1520
|
+
export function detectImageMimeFromMagic(raw: Buffer | Uint8Array | string | undefined): string | undefined {
|
|
1521
|
+
if (!raw) return undefined
|
|
1522
|
+
let bytes: Uint8Array | undefined
|
|
1523
|
+
if (typeof raw === 'string') {
|
|
1524
|
+
const trimmed = raw.trim()
|
|
1525
|
+
if (trimmed.startsWith('<svg') || (trimmed.startsWith('<?xml') && trimmed.includes('<svg'))) {
|
|
1526
|
+
return 'image/svg+xml'
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const prefix = trimmed.slice(0, 64).replace(/\s+/g, '')
|
|
1530
|
+
if (typeof Buffer !== 'undefined') {
|
|
1531
|
+
bytes = Buffer.from(prefix, 'base64')
|
|
1532
|
+
} else if (typeof atob !== 'undefined') {
|
|
1533
|
+
const bin = atob(prefix)
|
|
1534
|
+
bytes = new Uint8Array(bin.length)
|
|
1535
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
1536
|
+
}
|
|
1537
|
+
} catch {
|
|
1538
|
+
return undefined
|
|
1539
|
+
}
|
|
1540
|
+
} else if (raw instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(raw))) {
|
|
1541
|
+
bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (!bytes || bytes.length < 3) return undefined
|
|
1545
|
+
|
|
1546
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
1547
|
+
if (bytes.length >= 8 &&
|
|
1548
|
+
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
|
|
1549
|
+
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
|
|
1550
|
+
return 'image/png'
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// JPEG: FF D8 FF
|
|
1554
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
1555
|
+
return 'image/jpeg'
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// GIF: GIF87a or GIF89a (47 49 46 38)
|
|
1559
|
+
if (bytes.length >= 6 &&
|
|
1560
|
+
bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 &&
|
|
1561
|
+
(bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61) {
|
|
1562
|
+
return 'image/gif'
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// WebP: RIFF....WEBP (52 49 46 46 .... 57 45 42 50)
|
|
1566
|
+
if (bytes.length >= 12 &&
|
|
1567
|
+
bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
|
|
1568
|
+
bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
|
|
1569
|
+
return 'image/webp'
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// BMP: 42 4D ("BM")
|
|
1573
|
+
if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
|
|
1574
|
+
return 'image/bmp'
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ICO: 00 00 01 00
|
|
1578
|
+
if (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00) {
|
|
1579
|
+
return 'image/x-icon'
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// TIFF: II*. (49 49 2A 00) or MM.* (4D 4D 00 2A)
|
|
1583
|
+
if (bytes.length >= 4 &&
|
|
1584
|
+
((bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00) ||
|
|
1585
|
+
(bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a))) {
|
|
1586
|
+
return 'image/tiff'
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// AVIF / HEIC: ftyp box at offset 4 (bytes 4..7 === 'ftyp')
|
|
1590
|
+
if (bytes.length >= 12 &&
|
|
1591
|
+
bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {
|
|
1592
|
+
const brand = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]).toLowerCase()
|
|
1593
|
+
if (brand === 'avif' || brand === 'avis') return 'image/avif'
|
|
1594
|
+
if (brand === 'heic' || brand === 'heix' || brand === 'hevc' || brand === 'mif1' || brand === 'msf1') return 'image/heic'
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
return undefined
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function tryResolveFromAttachmentStore(
|
|
1601
|
+
store: Record<string, unknown>,
|
|
1602
|
+
id: string,
|
|
1603
|
+
context?: unknown,
|
|
1604
|
+
attachmentMeta?: unknown,
|
|
1605
|
+
): unknown {
|
|
1606
|
+
const idWithoutPrefix = id.startsWith('sha256:') ? id.slice('sha256:'.length) : undefined
|
|
1607
|
+
const idWithPrefix = id.startsWith('sha256:') ? id : `sha256:${id}`
|
|
1608
|
+
|
|
1609
|
+
const candidates: unknown[] = []
|
|
1610
|
+
if (attachmentMeta && typeof attachmentMeta === 'object') {
|
|
1611
|
+
candidates.push(attachmentMeta)
|
|
1612
|
+
}
|
|
1613
|
+
candidates.push({ attachmentId: idWithPrefix })
|
|
1614
|
+
if (idWithoutPrefix !== undefined) {
|
|
1615
|
+
candidates.push({ attachmentId: idWithoutPrefix })
|
|
1616
|
+
}
|
|
1617
|
+
candidates.push({ attachmentId: id })
|
|
1618
|
+
candidates.push({ id: idWithPrefix })
|
|
1619
|
+
candidates.push({ id })
|
|
1620
|
+
if (idWithoutPrefix !== undefined) {
|
|
1621
|
+
candidates.push({ id: idWithoutPrefix })
|
|
1622
|
+
}
|
|
1623
|
+
candidates.push(id)
|
|
1624
|
+
if (idWithoutPrefix !== undefined) {
|
|
1625
|
+
candidates.push(idWithoutPrefix)
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Cordis proxies wrap the underlying service instance; if a method call on the proxy fails
|
|
1629
|
+
// or loses this context, fall back to the unwrapped instance (never the class prototype,
|
|
1630
|
+
// which lacks instance state such as `this.root`).
|
|
1631
|
+
const cordisOriginal = Reflect.get(store, Symbol.for('cordis.original')) as Record<string, unknown> | undefined
|
|
1632
|
+
|
|
1633
|
+
// A. LocalAttachmentStore.imageHostPath(ref) -> synchronous local absolute path
|
|
1634
|
+
if (typeof store.imageHostPath === 'function') {
|
|
1635
|
+
const fn = store.imageHostPath as (ref: unknown) => unknown
|
|
1636
|
+
for (const cand of candidates) {
|
|
1637
|
+
if (cand === undefined) continue
|
|
1638
|
+
try {
|
|
1639
|
+
let hostPath: unknown
|
|
1640
|
+
try {
|
|
1641
|
+
hostPath = fn.call(store, cand)
|
|
1642
|
+
} catch {
|
|
1643
|
+
if (cordisOriginal && typeof cordisOriginal.imageHostPath === 'function') {
|
|
1644
|
+
hostPath = (cordisOriginal.imageHostPath as (ref: unknown) => unknown).call(cordisOriginal, cand)
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (hostPath && typeof (hostPath as Promise<unknown>).then === 'function') {
|
|
1648
|
+
if (typeof (hostPath as Promise<unknown>).catch === 'function') {
|
|
1649
|
+
(hostPath as Promise<unknown>).catch(() => {})
|
|
1650
|
+
}
|
|
1651
|
+
} else if (typeof hostPath === 'string' && hostPath.trim() !== '') {
|
|
1652
|
+
const fileContent = readImageFileFromContext(hostPath, context)
|
|
1653
|
+
return {
|
|
1654
|
+
path: hostPath,
|
|
1655
|
+
...(fileContent !== undefined ? { data: fileContent } : {}),
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
} catch {}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// B. LocalAttachmentStore.fileHostPath(ref) -> fallback for file-type attachments
|
|
1663
|
+
if (typeof store.fileHostPath === 'function') {
|
|
1664
|
+
const fn = store.fileHostPath as (ref: unknown) => unknown
|
|
1665
|
+
for (const cand of candidates) {
|
|
1666
|
+
if (cand === undefined) continue
|
|
1667
|
+
try {
|
|
1668
|
+
let hostPath: unknown
|
|
1669
|
+
try {
|
|
1670
|
+
hostPath = fn.call(store, cand)
|
|
1671
|
+
} catch {
|
|
1672
|
+
if (cordisOriginal && typeof cordisOriginal.fileHostPath === 'function') {
|
|
1673
|
+
hostPath = (cordisOriginal.fileHostPath as (ref: unknown) => unknown).call(cordisOriginal, cand)
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (hostPath && typeof (hostPath as Promise<unknown>).then === 'function') {
|
|
1677
|
+
if (typeof (hostPath as Promise<unknown>).catch === 'function') {
|
|
1678
|
+
(hostPath as Promise<unknown>).catch(() => {})
|
|
1679
|
+
}
|
|
1680
|
+
} else if (typeof hostPath === 'string' && hostPath.trim() !== '') {
|
|
1681
|
+
const fileContent = readImageFileFromContext(hostPath, context)
|
|
1682
|
+
return {
|
|
1683
|
+
path: hostPath,
|
|
1684
|
+
...(fileContent !== undefined ? { data: fileContent } : {}),
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} catch {}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// C. LocalAttachmentStore.readImage(ref) -> { ref, data: Uint8Array }
|
|
1692
|
+
if (typeof store.readImage === 'function') {
|
|
1693
|
+
const fn = store.readImage as (ref: unknown) => unknown
|
|
1694
|
+
for (const cand of candidates) {
|
|
1695
|
+
if (cand === undefined) continue
|
|
1696
|
+
try {
|
|
1697
|
+
let res: unknown
|
|
1698
|
+
try {
|
|
1699
|
+
res = fn.call(store, cand)
|
|
1700
|
+
} catch {
|
|
1701
|
+
if (cordisOriginal && typeof cordisOriginal.readImage === 'function') {
|
|
1702
|
+
res = (cordisOriginal.readImage as (ref: unknown) => unknown).call(cordisOriginal, cand)
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (res && typeof res === 'object') {
|
|
1706
|
+
// If it's a Promise (async), safely attach catch to avoid unhandledRejection crashes
|
|
1707
|
+
if (typeof (res as Promise<unknown>).then === 'function') {
|
|
1708
|
+
if (typeof (res as Promise<unknown>).catch === 'function') {
|
|
1709
|
+
(res as Promise<unknown>).catch(() => {})
|
|
1710
|
+
}
|
|
1711
|
+
} else {
|
|
1712
|
+
if (res instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(res))) {
|
|
1713
|
+
return res
|
|
1714
|
+
}
|
|
1715
|
+
const data = (res as { data?: unknown }).data
|
|
1716
|
+
if (data instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) || typeof data === 'string') {
|
|
1717
|
+
return { data }
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
} catch {}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// D. readAttachment
|
|
1726
|
+
if (typeof store.readAttachment === 'function') {
|
|
1727
|
+
try {
|
|
1728
|
+
let res: unknown
|
|
1729
|
+
try {
|
|
1730
|
+
res = (store.readAttachment as (id: string) => unknown).call(store, id)
|
|
1731
|
+
} catch {
|
|
1732
|
+
if (cordisOriginal && typeof cordisOriginal.readAttachment === 'function') {
|
|
1733
|
+
res = (cordisOriginal.readAttachment as (id: string) => unknown).call(cordisOriginal, id)
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (res && typeof (res as Promise<unknown>).then === 'function') {
|
|
1737
|
+
if (typeof (res as Promise<unknown>).catch === 'function') {
|
|
1738
|
+
(res as Promise<unknown>).catch(() => {})
|
|
1739
|
+
}
|
|
1740
|
+
} else if (res !== undefined) {
|
|
1741
|
+
return res
|
|
1742
|
+
}
|
|
1743
|
+
} catch {}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// E. getAttachment
|
|
1747
|
+
if (typeof store.getAttachment === 'function') {
|
|
1748
|
+
try {
|
|
1749
|
+
let res: unknown
|
|
1750
|
+
try {
|
|
1751
|
+
res = (store.getAttachment as (id: string) => unknown).call(store, id)
|
|
1752
|
+
} catch {
|
|
1753
|
+
if (cordisOriginal && typeof cordisOriginal.getAttachment === 'function') {
|
|
1754
|
+
res = (cordisOriginal.getAttachment as (id: string) => unknown).call(cordisOriginal, id)
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (res && typeof (res as Promise<unknown>).then === 'function') {
|
|
1758
|
+
if (typeof (res as Promise<unknown>).catch === 'function') {
|
|
1759
|
+
(res as Promise<unknown>).catch(() => {})
|
|
1760
|
+
}
|
|
1761
|
+
} else if (res !== undefined) {
|
|
1762
|
+
return res
|
|
1763
|
+
}
|
|
1764
|
+
} catch {}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return undefined
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function tryStoreFreeDiskAttachment(id: string, context?: unknown): unknown {
|
|
1771
|
+
if (typeof id !== 'string') return undefined
|
|
1772
|
+
let hash: string | undefined
|
|
1773
|
+
const trimmed = id.trim()
|
|
1774
|
+
if (trimmed.startsWith('sha256:')) {
|
|
1775
|
+
hash = trimmed.slice('sha256:'.length).toLowerCase()
|
|
1776
|
+
} else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
|
1777
|
+
hash = trimmed.toLowerCase()
|
|
1778
|
+
}
|
|
1779
|
+
if (!hash || hash.length < 2) return undefined
|
|
1780
|
+
|
|
1781
|
+
const prefix = hash.slice(0, 2)
|
|
1782
|
+
const relPaths = [
|
|
1783
|
+
`attachments/v1/objects/${prefix}/${hash}`,
|
|
1784
|
+
`attachments/v1/file-objects/${prefix}/${hash}`,
|
|
1785
|
+
]
|
|
1786
|
+
|
|
1787
|
+
const candidatePaths: string[] = []
|
|
1788
|
+
|
|
1789
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
1790
|
+
const env = process.env
|
|
1791
|
+
if (env.DSH_NTR_STATE_DIR) {
|
|
1792
|
+
const stateDir = env.DSH_NTR_STATE_DIR.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
1793
|
+
const hostDir = stateDir.endsWith('/dsh-ntr') ? stateDir.slice(0, -'/dsh-ntr'.length) : stateDir
|
|
1794
|
+
for (const rel of relPaths) candidatePaths.push(`${hostDir}/${rel}`)
|
|
1795
|
+
}
|
|
1796
|
+
if (env.DSH_DIR) {
|
|
1797
|
+
const dshDir = env.DSH_DIR.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
1798
|
+
for (const rel of relPaths) candidatePaths.push(`${dshDir}/${rel}`)
|
|
1799
|
+
}
|
|
1800
|
+
const home = env.HOME || env.USERPROFILE || (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined)
|
|
1801
|
+
if (home) {
|
|
1802
|
+
const cleanHome = home.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
1803
|
+
for (const rel of relPaths) {
|
|
1804
|
+
candidatePaths.push(`${cleanHome}/.dsh/${rel}`)
|
|
1805
|
+
candidatePaths.push(`${cleanHome}\\.dsh\\${rel.replaceAll('/', '\\')}`)
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
for (const rel of relPaths) candidatePaths.push(`~/.dsh/${rel}`)
|
|
1810
|
+
|
|
1811
|
+
for (const p of candidatePaths) {
|
|
1812
|
+
const fileContent = readImageFileFromContext(p, context)
|
|
1813
|
+
if (fileContent !== undefined) {
|
|
1814
|
+
return {
|
|
1815
|
+
path: p,
|
|
1816
|
+
data: fileContent,
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return undefined
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
export function resolveImageAttachmentFromContext(
|
|
1825
|
+
id: string,
|
|
1826
|
+
context?: unknown,
|
|
1827
|
+
attachmentMeta?: unknown,
|
|
1828
|
+
rootContext?: unknown,
|
|
1829
|
+
): unknown {
|
|
1830
|
+
const root = rootContext ?? context
|
|
1831
|
+
if (context && typeof context === 'object') {
|
|
1832
|
+
const ctx = context as Record<string, unknown>
|
|
1833
|
+
|
|
1834
|
+
const direct = tryResolveFromAttachmentStore(ctx, id, root, attachmentMeta)
|
|
1835
|
+
if (direct !== undefined) return direct
|
|
1836
|
+
|
|
1837
|
+
if (typeof ctx.get === 'function') {
|
|
1838
|
+
try {
|
|
1839
|
+
const svc = (ctx.get as (name: string) => unknown)('attachments') ?? (ctx.get as (name: string) => unknown)('attachmentService')
|
|
1840
|
+
if (svc && typeof svc === 'object') {
|
|
1841
|
+
const res = resolveImageAttachmentFromContext(id, svc, attachmentMeta, root)
|
|
1842
|
+
if (res !== undefined) return res
|
|
1843
|
+
}
|
|
1844
|
+
} catch {}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
try {
|
|
1848
|
+
if (ctx.attachmentService && typeof ctx.attachmentService === 'object') {
|
|
1849
|
+
const res = resolveImageAttachmentFromContext(id, ctx.attachmentService, attachmentMeta, root)
|
|
1850
|
+
if (res !== undefined) return res
|
|
1851
|
+
}
|
|
1852
|
+
} catch {}
|
|
1853
|
+
|
|
1854
|
+
try {
|
|
1855
|
+
const attStore = ctx.attachments
|
|
1856
|
+
if (attStore) {
|
|
1857
|
+
if (typeof attStore === 'object') {
|
|
1858
|
+
const fromStore = tryResolveFromAttachmentStore(attStore as Record<string, unknown>, id, root, attachmentMeta)
|
|
1859
|
+
if (fromStore !== undefined) return fromStore
|
|
1860
|
+
}
|
|
1861
|
+
if (attStore instanceof Map || typeof (attStore as { get?: unknown }).get === 'function') {
|
|
1862
|
+
try { return (attStore as Map<string, unknown>).get(id) } catch {}
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof attStore === 'object' && Object.prototype.hasOwnProperty.call(attStore, id)) {
|
|
1865
|
+
return (attStore as Record<string, unknown>)[id]
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
} catch {}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
return tryStoreFreeDiskAttachment(id, root)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function toBase64(data: Buffer | Uint8Array | string): string {
|
|
1875
|
+
if (typeof data === 'string') return data.replace(/\s+/g, '')
|
|
1876
|
+
if (typeof Buffer !== 'undefined') return Buffer.from(data).toString('base64')
|
|
1877
|
+
let binary = ''
|
|
1878
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
|
|
1879
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1880
|
+
binary += String.fromCharCode(bytes[i])
|
|
1881
|
+
}
|
|
1882
|
+
return btoa(binary)
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Extract an image payload (Base64 data + MIME type) from an image block and optional context.
|
|
1887
|
+
*/
|
|
1888
|
+
export function resolveImageContent(block: unknown, context?: unknown): PromptImageContent | undefined {
|
|
1889
|
+
if (!block || typeof block !== 'object') return undefined
|
|
1890
|
+
const b = block as Record<string, unknown>
|
|
1891
|
+
const attRecord = b.attachment && typeof b.attachment === 'object' ? (b.attachment as Record<string, unknown>) : undefined
|
|
1892
|
+
const isExplicitImageBlock = b.type === 'image' || b.type === 'image_url' || b.type === 'attachment'
|
|
1893
|
+
const explicitMime = (typeof attRecord?.mediaType === 'string' && attRecord.mediaType.trim().toLowerCase()) ||
|
|
1894
|
+
(typeof attRecord?.mimeType === 'string' && attRecord.mimeType.trim().toLowerCase()) ||
|
|
1895
|
+
(typeof b.mediaType === 'string' && b.mediaType.trim().toLowerCase()) ||
|
|
1896
|
+
(typeof b.mimeType === 'string' && b.mimeType.trim().toLowerCase()) ||
|
|
1897
|
+
(typeof b.contentType === 'string' && b.contentType.trim().toLowerCase()) ||
|
|
1898
|
+
undefined
|
|
1899
|
+
|
|
1900
|
+
// 1. Direct base64 data: { data: string | Buffer | Uint8Array, mimeType?: string }
|
|
1901
|
+
if (typeof b.data === 'string' && b.data.trim() !== '') {
|
|
1902
|
+
if (b.data.startsWith('data:')) {
|
|
1903
|
+
const parsed = parseImageDataUrl(b.data)
|
|
1904
|
+
if (parsed) {
|
|
1905
|
+
const mimeType = explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png'
|
|
1906
|
+
return { type: 'image', data: parsed.data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
1907
|
+
}
|
|
1908
|
+
return undefined
|
|
1909
|
+
}
|
|
1910
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1911
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
1912
|
+
return {
|
|
1913
|
+
type: 'image',
|
|
1914
|
+
data: b.data.trim().replace(/\s+/g, ''),
|
|
1915
|
+
mimeType,
|
|
1916
|
+
...(typeof b.uri === 'string' ? { uri: b.uri } : {}),
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.data)) {
|
|
1921
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1922
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
1923
|
+
return { type: 'image', data: b.data.toString('base64'), mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (b.data instanceof Uint8Array) {
|
|
1927
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1928
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
1929
|
+
return { type: 'image', data: toBase64(b.data), mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// 2. Anthropic style: source: { type: 'base64', media_type: string, data: string }
|
|
1934
|
+
if (b.source && typeof b.source === 'object') {
|
|
1935
|
+
const src = b.source as Record<string, unknown>
|
|
1936
|
+
const srcMime = (typeof src.media_type === 'string' && src.media_type.trim().toLowerCase()) ||
|
|
1937
|
+
(typeof src.mimeType === 'string' && src.mimeType.trim().toLowerCase()) ||
|
|
1938
|
+
explicitMime ||
|
|
1939
|
+
(typeof src.data === 'string' || src.data instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(src.data)) ? detectImageMimeFromMagic(src.data as Buffer | Uint8Array | string) : undefined) ||
|
|
1940
|
+
'image/png'
|
|
1941
|
+
if (srcMime.startsWith('image/')) {
|
|
1942
|
+
if (typeof src.data === 'string' && src.data.trim() !== '') {
|
|
1943
|
+
return {
|
|
1944
|
+
type: 'image',
|
|
1945
|
+
data: src.data.trim().replace(/\s+/g, ''),
|
|
1946
|
+
mimeType: srcMime,
|
|
1947
|
+
...(typeof b.uri === 'string' ? { uri: b.uri } : {}),
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(src.data)) {
|
|
1951
|
+
return { type: 'image', data: src.data.toString('base64'), mimeType: srcMime, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
1952
|
+
}
|
|
1953
|
+
if (src.data instanceof Uint8Array) {
|
|
1954
|
+
return { type: 'image', data: toBase64(src.data), mimeType: srcMime, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// 3. Image property: { image: string | Buffer | Uint8Array }
|
|
1960
|
+
if (typeof b.image === 'string' && b.image.trim() !== '') {
|
|
1961
|
+
if (b.image.startsWith('data:')) {
|
|
1962
|
+
const parsed = parseImageDataUrl(b.image)
|
|
1963
|
+
if (parsed) return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png' }
|
|
1964
|
+
return undefined
|
|
1965
|
+
}
|
|
1966
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1967
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
1968
|
+
return { type: 'image', data: b.image.trim().replace(/\s+/g, ''), mimeType }
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.image)) {
|
|
1972
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1973
|
+
if (mimeType && mimeType.startsWith('image/')) return { type: 'image', data: b.image.toString('base64'), mimeType }
|
|
1974
|
+
}
|
|
1975
|
+
if (b.image instanceof Uint8Array) {
|
|
1976
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
1977
|
+
if (mimeType && mimeType.startsWith('image/')) return { type: 'image', data: toBase64(b.image), mimeType }
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// 4. OpenAI style image_url: { image_url: string | { url: string } }
|
|
1981
|
+
if (b.image_url) {
|
|
1982
|
+
const urlStr = typeof b.image_url === 'string'
|
|
1983
|
+
? b.image_url
|
|
1984
|
+
: (typeof b.image_url === 'object' && b.image_url !== null ? (b.image_url as Record<string, unknown>).url : undefined)
|
|
1985
|
+
if (typeof urlStr === 'string' && urlStr.trim() !== '') {
|
|
1986
|
+
if (urlStr.startsWith('data:')) {
|
|
1987
|
+
const parsed = parseImageDataUrl(urlStr)
|
|
1988
|
+
if (parsed) return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png', uri: urlStr }
|
|
1989
|
+
return undefined
|
|
1990
|
+
}
|
|
1991
|
+
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
|
|
1992
|
+
const mimeType = explicitMime || guessImageMimeType(urlStr) || 'image/png'
|
|
1993
|
+
return { type: 'image', data: '', mimeType, uri: urlStr }
|
|
1994
|
+
}
|
|
1995
|
+
if (urlStr.startsWith('file://') || isLocalFilePath(urlStr)) {
|
|
1996
|
+
const fileContent = readImageFileFromContext(urlStr, context)
|
|
1997
|
+
if (fileContent !== undefined) {
|
|
1998
|
+
let data: string
|
|
1999
|
+
let resolvedMime: string | undefined
|
|
2000
|
+
if (typeof fileContent === 'string') {
|
|
2001
|
+
if (fileContent.startsWith('data:')) {
|
|
2002
|
+
const parsed = parseImageDataUrl(fileContent)
|
|
2003
|
+
if (parsed) {
|
|
2004
|
+
data = parsed.data
|
|
2005
|
+
resolvedMime = parsed.mimeType
|
|
2006
|
+
} else {
|
|
2007
|
+
return undefined
|
|
2008
|
+
}
|
|
2009
|
+
} else {
|
|
2010
|
+
data = fileContent.trim().replace(/\s+/g, '')
|
|
2011
|
+
}
|
|
2012
|
+
} else {
|
|
2013
|
+
data = toBase64(fileContent)
|
|
2014
|
+
}
|
|
2015
|
+
const mimeType = explicitMime || resolvedMime || guessImageMimeType(urlStr) || detectImageMimeFromMagic(fileContent) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
2016
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2017
|
+
return { type: 'image', data, mimeType, uri: urlStr }
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// 5. Direct buffer / content on block: { buffer: Buffer } or { content: Buffer }
|
|
2025
|
+
if (b.buffer && (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.buffer) || b.buffer instanceof Uint8Array)) {
|
|
2026
|
+
const data = typeof Buffer !== 'undefined' && Buffer.isBuffer(b.buffer) ? b.buffer.toString('base64') : toBase64(b.buffer as Uint8Array)
|
|
2027
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.buffer as Buffer | Uint8Array) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
2028
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2029
|
+
return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (b.content && (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.content) || b.content instanceof Uint8Array)) {
|
|
2033
|
+
const data = typeof Buffer !== 'undefined' && Buffer.isBuffer(b.content) ? b.content.toString('base64') : toBase64(b.content as Uint8Array)
|
|
2034
|
+
const mimeType = explicitMime || detectImageMimeFromMagic(b.content as Buffer | Uint8Array) || (isExplicitImageBlock ? 'image/png' : undefined)
|
|
2035
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2036
|
+
return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// 6. Data URL or Remote URL in `url`
|
|
2041
|
+
if (typeof b.url === 'string') {
|
|
2042
|
+
if (b.url.startsWith('data:')) {
|
|
2043
|
+
const parsed = parseImageDataUrl(b.url)
|
|
2044
|
+
if (parsed) return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png', uri: b.url }
|
|
2045
|
+
return undefined
|
|
2046
|
+
}
|
|
2047
|
+
if (b.url.startsWith('http://') || b.url.startsWith('https://')) {
|
|
2048
|
+
const mimeType = explicitMime || guessImageMimeType(b.url) || 'image/png'
|
|
2049
|
+
return { type: 'image', data: '', mimeType, uri: b.url }
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// 7. File path in `path`, `filePath`, or `url: file://...`
|
|
2054
|
+
let filePath: string | undefined
|
|
2055
|
+
if (typeof b.path === 'string' && b.path !== '') filePath = b.path
|
|
2056
|
+
else if (typeof b.filePath === 'string' && b.filePath !== '') filePath = b.filePath
|
|
2057
|
+
else if (typeof b.url === 'string' && (b.url.startsWith('file://') || isLocalFilePath(b.url))) filePath = b.url
|
|
2058
|
+
|
|
2059
|
+
if (filePath !== undefined) {
|
|
2060
|
+
const fileContent = readImageFileFromContext(filePath, context)
|
|
2061
|
+
if (fileContent !== undefined) {
|
|
2062
|
+
let data: string
|
|
2063
|
+
let resolvedMime: string | undefined
|
|
2064
|
+
if (typeof fileContent === 'string') {
|
|
2065
|
+
if (fileContent.startsWith('data:')) {
|
|
2066
|
+
const parsed = parseImageDataUrl(fileContent)
|
|
2067
|
+
if (parsed) {
|
|
2068
|
+
data = parsed.data
|
|
2069
|
+
resolvedMime = parsed.mimeType
|
|
2070
|
+
} else {
|
|
2071
|
+
return undefined
|
|
2072
|
+
}
|
|
2073
|
+
} else {
|
|
2074
|
+
data = fileContent.trim().replace(/\s+/g, '')
|
|
2075
|
+
}
|
|
2076
|
+
} else {
|
|
2077
|
+
data = toBase64(fileContent)
|
|
2078
|
+
}
|
|
2079
|
+
const mimeType = explicitMime ||
|
|
2080
|
+
resolvedMime ||
|
|
2081
|
+
guessImageMimeType(filePath) ||
|
|
2082
|
+
detectImageMimeFromMagic(fileContent) ||
|
|
2083
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2084
|
+
|
|
2085
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2086
|
+
let uri = typeof b.url === 'string' ? b.url : undefined
|
|
2087
|
+
if (!uri) {
|
|
2088
|
+
if (filePath.startsWith('file://')) {
|
|
2089
|
+
uri = filePath
|
|
2090
|
+
} else if (isAbsolutePath(filePath)) {
|
|
2091
|
+
if (filePath.startsWith('//') || filePath.startsWith('\\\\')) {
|
|
2092
|
+
uri = `file://${filePath.replace(/^[\/\\]+/, '').replace(/\\/g, '/')}`
|
|
2093
|
+
} else if (/^[a-zA-Z]:/.test(filePath)) {
|
|
2094
|
+
uri = `file:///${filePath.replace(/\\/g, '/')}`
|
|
2095
|
+
} else {
|
|
2096
|
+
uri = `file://${filePath.startsWith('/') ? filePath : `/${filePath}`}`
|
|
2097
|
+
}
|
|
2098
|
+
} else {
|
|
2099
|
+
uri = filePath
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return {
|
|
2103
|
+
type: 'image',
|
|
2104
|
+
data,
|
|
2105
|
+
mimeType,
|
|
2106
|
+
uri,
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// 8. Attachment resolution via context
|
|
2113
|
+
let attachmentId: string | undefined
|
|
2114
|
+
if (attRecord && typeof attRecord.attachmentId === 'string' && attRecord.attachmentId.trim() !== '') {
|
|
2115
|
+
attachmentId = attRecord.attachmentId.trim()
|
|
2116
|
+
} else if (typeof b.attachmentId === 'string' && b.attachmentId.trim() !== '') {
|
|
2117
|
+
attachmentId = b.attachmentId.trim()
|
|
2118
|
+
} else if (attRecord && typeof attRecord.id === 'string' && attRecord.id.trim() !== '') {
|
|
2119
|
+
attachmentId = attRecord.id.trim()
|
|
2120
|
+
} else if (typeof b.id === 'string' && b.id.trim() !== '') {
|
|
2121
|
+
attachmentId = b.id.trim()
|
|
2122
|
+
} else if (typeof b.attachment === 'string' && b.attachment.trim() !== '') {
|
|
2123
|
+
attachmentId = b.attachment.trim()
|
|
2124
|
+
} else if (typeof b.url === 'string' && b.url.startsWith('attachment:')) {
|
|
2125
|
+
attachmentId = b.url.replace(/^attachment:\/\/|^attachment:/, '')
|
|
2126
|
+
} else if (typeof b.image_url === 'string' && b.image_url.startsWith('attachment:')) {
|
|
2127
|
+
attachmentId = b.image_url.replace(/^attachment:\/\/|^attachment:/, '')
|
|
2128
|
+
} else if (b.image_url && typeof b.image_url === 'object' && typeof (b.image_url as Record<string, unknown>).url === 'string' && ((b.image_url as Record<string, unknown>).url as string).startsWith('attachment:')) {
|
|
2129
|
+
attachmentId = ((b.image_url as Record<string, unknown>).url as string).replace(/^attachment:\/\/|^attachment:/, '')
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (attachmentId !== undefined) {
|
|
2133
|
+
const rawAtt = resolveImageAttachmentFromContext(attachmentId, context, b.attachment)
|
|
2134
|
+
if (rawAtt !== undefined) {
|
|
2135
|
+
if (typeof rawAtt === 'string') {
|
|
2136
|
+
if (rawAtt.startsWith('data:')) {
|
|
2137
|
+
const parsed = parseImageDataUrl(rawAtt)
|
|
2138
|
+
if (parsed) {
|
|
2139
|
+
const mimeType = explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png'
|
|
2140
|
+
return {
|
|
2141
|
+
type: 'image',
|
|
2142
|
+
data: parsed.data,
|
|
2143
|
+
mimeType,
|
|
2144
|
+
uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return undefined
|
|
2148
|
+
} else if (rawAtt.startsWith('file://') || isLocalFilePath(rawAtt)) {
|
|
2149
|
+
const fileContent = readImageFileFromContext(rawAtt, context)
|
|
2150
|
+
if (fileContent !== undefined) {
|
|
2151
|
+
let data: string
|
|
2152
|
+
let resolvedMime: string | undefined
|
|
2153
|
+
if (typeof fileContent === 'string') {
|
|
2154
|
+
if (fileContent.startsWith('data:')) {
|
|
2155
|
+
const parsed = parseImageDataUrl(fileContent)
|
|
2156
|
+
if (parsed) {
|
|
2157
|
+
data = parsed.data
|
|
2158
|
+
resolvedMime = parsed.mimeType
|
|
2159
|
+
} else {
|
|
2160
|
+
return undefined
|
|
2161
|
+
}
|
|
2162
|
+
} else {
|
|
2163
|
+
data = fileContent.trim().replace(/\s+/g, '')
|
|
2164
|
+
}
|
|
2165
|
+
} else {
|
|
2166
|
+
data = toBase64(fileContent)
|
|
2167
|
+
}
|
|
2168
|
+
const mimeType = explicitMime ||
|
|
2169
|
+
resolvedMime ||
|
|
2170
|
+
(typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
|
|
2171
|
+
guessImageMimeType(rawAtt) ||
|
|
2172
|
+
detectImageMimeFromMagic(fileContent) ||
|
|
2173
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2174
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2175
|
+
return { type: 'image', data, mimeType, uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}` }
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
const mimeType = explicitMime ||
|
|
2180
|
+
(typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
|
|
2181
|
+
guessImageMimeType(attachmentId) ||
|
|
2182
|
+
detectImageMimeFromMagic(rawAtt) ||
|
|
2183
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2184
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2185
|
+
return { type: 'image', data: rawAtt.trim().replace(/\s+/g, ''), mimeType, uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}` }
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (typeof rawAtt === 'object' && rawAtt !== null) {
|
|
2189
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(rawAtt)) {
|
|
2190
|
+
const mimeType = explicitMime ||
|
|
2191
|
+
(typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
|
|
2192
|
+
guessImageMimeType(attachmentId) ||
|
|
2193
|
+
detectImageMimeFromMagic(rawAtt) ||
|
|
2194
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2195
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2196
|
+
return {
|
|
2197
|
+
type: 'image',
|
|
2198
|
+
data: rawAtt.toString('base64'),
|
|
2199
|
+
mimeType,
|
|
2200
|
+
uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (rawAtt instanceof Uint8Array) {
|
|
2205
|
+
const mimeType = explicitMime ||
|
|
2206
|
+
(typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
|
|
2207
|
+
guessImageMimeType(attachmentId) ||
|
|
2208
|
+
detectImageMimeFromMagic(rawAtt) ||
|
|
2209
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2210
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2211
|
+
return {
|
|
2212
|
+
type: 'image',
|
|
2213
|
+
data: toBase64(rawAtt),
|
|
2214
|
+
mimeType,
|
|
2215
|
+
uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
const attObj = rawAtt as Record<string, unknown>
|
|
2220
|
+
let data: string | undefined
|
|
2221
|
+
let resolvedMime: string | undefined
|
|
2222
|
+
|
|
2223
|
+
const attPath = typeof attObj.path === 'string' && attObj.path !== ''
|
|
2224
|
+
? attObj.path
|
|
2225
|
+
: (typeof attObj.filePath === 'string' && attObj.filePath !== ''
|
|
2226
|
+
? attObj.filePath
|
|
2227
|
+
: (typeof attObj.url === 'string' && (attObj.url.startsWith('file://') || isLocalFilePath(attObj.url)) ? attObj.url : undefined))
|
|
2228
|
+
|
|
2229
|
+
let fileContentForMagic: Buffer | Uint8Array | string | undefined
|
|
2230
|
+
|
|
2231
|
+
if (attPath !== undefined) {
|
|
2232
|
+
const fileContent = readImageFileFromContext(attPath, context)
|
|
2233
|
+
if (fileContent !== undefined) {
|
|
2234
|
+
fileContentForMagic = fileContent
|
|
2235
|
+
if (typeof fileContent === 'string') {
|
|
2236
|
+
if (fileContent.startsWith('data:')) {
|
|
2237
|
+
const parsed = parseImageDataUrl(fileContent)
|
|
2238
|
+
if (parsed) {
|
|
2239
|
+
data = parsed.data
|
|
2240
|
+
resolvedMime = parsed.mimeType
|
|
2241
|
+
}
|
|
2242
|
+
} else {
|
|
2243
|
+
data = fileContent.trim().replace(/\s+/g, '')
|
|
2244
|
+
}
|
|
2245
|
+
} else {
|
|
2246
|
+
data = toBase64(fileContent)
|
|
2247
|
+
}
|
|
2248
|
+
if (!resolvedMime) resolvedMime = guessImageMimeType(attPath)
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
if (data === undefined) {
|
|
2253
|
+
if (typeof attObj.data === 'string' && attObj.data.trim() !== '') {
|
|
2254
|
+
if (attObj.data.startsWith('data:')) {
|
|
2255
|
+
const parsed = parseImageDataUrl(attObj.data)
|
|
2256
|
+
if (parsed) {
|
|
2257
|
+
data = parsed.data
|
|
2258
|
+
resolvedMime = parsed.mimeType
|
|
2259
|
+
}
|
|
2260
|
+
} else {
|
|
2261
|
+
data = attObj.data.trim().replace(/\s+/g, '')
|
|
2262
|
+
fileContentForMagic = attObj.data
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.data)) {
|
|
2266
|
+
data = attObj.data.toString('base64')
|
|
2267
|
+
fileContentForMagic = attObj.data
|
|
2268
|
+
}
|
|
2269
|
+
else if (attObj.data instanceof Uint8Array) {
|
|
2270
|
+
data = toBase64(attObj.data)
|
|
2271
|
+
fileContentForMagic = attObj.data
|
|
2272
|
+
}
|
|
2273
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.buffer)) {
|
|
2274
|
+
data = attObj.buffer.toString('base64')
|
|
2275
|
+
fileContentForMagic = attObj.buffer
|
|
2276
|
+
}
|
|
2277
|
+
else if (attObj.buffer instanceof Uint8Array) {
|
|
2278
|
+
data = toBase64(attObj.buffer)
|
|
2279
|
+
fileContentForMagic = attObj.buffer
|
|
2280
|
+
}
|
|
2281
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.content)) {
|
|
2282
|
+
data = attObj.content.toString('base64')
|
|
2283
|
+
fileContentForMagic = attObj.content
|
|
2284
|
+
}
|
|
2285
|
+
else if (attObj.content instanceof Uint8Array) {
|
|
2286
|
+
data = toBase64(attObj.content)
|
|
2287
|
+
fileContentForMagic = attObj.content
|
|
2288
|
+
}
|
|
2289
|
+
else if (typeof attObj.content === 'string') {
|
|
2290
|
+
if (attObj.content.startsWith('data:')) {
|
|
2291
|
+
const parsed = parseImageDataUrl(attObj.content)
|
|
2292
|
+
if (parsed) {
|
|
2293
|
+
data = parsed.data
|
|
2294
|
+
resolvedMime = parsed.mimeType
|
|
2295
|
+
}
|
|
2296
|
+
} else {
|
|
2297
|
+
data = attObj.content.trim().replace(/\s+/g, '')
|
|
2298
|
+
fileContentForMagic = attObj.content
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (data !== undefined) {
|
|
2304
|
+
const mimeType = explicitMime ||
|
|
2305
|
+
(typeof attObj.mediaType === 'string' && attObj.mediaType.toLowerCase()) ||
|
|
2306
|
+
(typeof attObj.mimeType === 'string' && attObj.mimeType.toLowerCase()) ||
|
|
2307
|
+
resolvedMime ||
|
|
2308
|
+
(typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
|
|
2309
|
+
(typeof attObj.name === 'string' ? guessImageMimeType(attObj.name as string) : undefined) ||
|
|
2310
|
+
guessImageMimeType(attachmentId) ||
|
|
2311
|
+
detectImageMimeFromMagic(fileContentForMagic ?? data) ||
|
|
2312
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2313
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2314
|
+
return {
|
|
2315
|
+
type: 'image',
|
|
2316
|
+
data,
|
|
2317
|
+
mimeType,
|
|
2318
|
+
uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// 9. Direct b.attachment object
|
|
2327
|
+
if (b.attachment && typeof b.attachment === 'object') {
|
|
2328
|
+
const attObj = b.attachment as Record<string, unknown>
|
|
2329
|
+
let data: string | undefined
|
|
2330
|
+
let resolvedMime: string | undefined
|
|
2331
|
+
|
|
2332
|
+
const attPath = typeof attObj.path === 'string' && attObj.path !== ''
|
|
2333
|
+
? attObj.path
|
|
2334
|
+
: (typeof attObj.filePath === 'string' && attObj.filePath !== ''
|
|
2335
|
+
? attObj.filePath
|
|
2336
|
+
: (typeof attObj.url === 'string' && (attObj.url.startsWith('file://') || isLocalFilePath(attObj.url)) ? attObj.url : undefined))
|
|
2337
|
+
|
|
2338
|
+
let fileContentForMagic: Buffer | Uint8Array | string | undefined
|
|
2339
|
+
|
|
2340
|
+
if (attPath !== undefined) {
|
|
2341
|
+
const fileContent = readImageFileFromContext(attPath, context)
|
|
2342
|
+
if (fileContent !== undefined) {
|
|
2343
|
+
fileContentForMagic = fileContent
|
|
2344
|
+
if (typeof fileContent === 'string') {
|
|
2345
|
+
if (fileContent.startsWith('data:')) {
|
|
2346
|
+
const parsed = parseImageDataUrl(fileContent)
|
|
2347
|
+
if (parsed) {
|
|
2348
|
+
data = parsed.data
|
|
2349
|
+
resolvedMime = parsed.mimeType
|
|
2350
|
+
}
|
|
2351
|
+
} else {
|
|
2352
|
+
data = fileContent.trim().replace(/\s+/g, '')
|
|
2353
|
+
}
|
|
2354
|
+
} else {
|
|
2355
|
+
data = toBase64(fileContent)
|
|
2356
|
+
}
|
|
2357
|
+
if (!resolvedMime) resolvedMime = guessImageMimeType(attPath)
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (data === undefined) {
|
|
2362
|
+
if (typeof attObj.data === 'string' && attObj.data.trim() !== '') {
|
|
2363
|
+
if (attObj.data.startsWith('data:')) {
|
|
2364
|
+
const parsed = parseImageDataUrl(attObj.data)
|
|
2365
|
+
if (parsed) {
|
|
2366
|
+
data = parsed.data
|
|
2367
|
+
resolvedMime = parsed.mimeType
|
|
2368
|
+
}
|
|
2369
|
+
} else {
|
|
2370
|
+
data = attObj.data.trim().replace(/\s+/g, '')
|
|
2371
|
+
fileContentForMagic = attObj.data
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.data)) {
|
|
2375
|
+
data = attObj.data.toString('base64')
|
|
2376
|
+
fileContentForMagic = attObj.data
|
|
2377
|
+
}
|
|
2378
|
+
else if (attObj.data instanceof Uint8Array) {
|
|
2379
|
+
data = toBase64(attObj.data)
|
|
2380
|
+
fileContentForMagic = attObj.data
|
|
2381
|
+
}
|
|
2382
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.buffer)) {
|
|
2383
|
+
data = attObj.buffer.toString('base64')
|
|
2384
|
+
fileContentForMagic = attObj.buffer
|
|
2385
|
+
}
|
|
2386
|
+
else if (attObj.buffer instanceof Uint8Array) {
|
|
2387
|
+
data = toBase64(attObj.buffer)
|
|
2388
|
+
fileContentForMagic = attObj.buffer
|
|
2389
|
+
}
|
|
2390
|
+
else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.content)) {
|
|
2391
|
+
data = attObj.content.toString('base64')
|
|
2392
|
+
fileContentForMagic = attObj.content
|
|
2393
|
+
}
|
|
2394
|
+
else if (attObj.content instanceof Uint8Array) {
|
|
2395
|
+
data = toBase64(attObj.content)
|
|
2396
|
+
fileContentForMagic = attObj.content
|
|
2397
|
+
}
|
|
2398
|
+
else if (typeof attObj.content === 'string') {
|
|
2399
|
+
if (attObj.content.startsWith('data:')) {
|
|
2400
|
+
const parsed = parseImageDataUrl(attObj.content)
|
|
2401
|
+
if (parsed) {
|
|
2402
|
+
data = parsed.data
|
|
2403
|
+
resolvedMime = parsed.mimeType
|
|
2404
|
+
}
|
|
2405
|
+
} else {
|
|
2406
|
+
data = attObj.content.trim().replace(/\s+/g, '')
|
|
2407
|
+
fileContentForMagic = attObj.content
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
if (data !== undefined) {
|
|
2413
|
+
const mimeType = explicitMime ||
|
|
2414
|
+
(typeof attObj.mediaType === 'string' && attObj.mediaType.toLowerCase()) ||
|
|
2415
|
+
(typeof attObj.mimeType === 'string' && attObj.mimeType.toLowerCase()) ||
|
|
2416
|
+
resolvedMime ||
|
|
2417
|
+
(typeof attObj.name === 'string' ? guessImageMimeType(attObj.name as string) : undefined) ||
|
|
2418
|
+
(attPath ? guessImageMimeType(attPath) : undefined) ||
|
|
2419
|
+
detectImageMimeFromMagic(fileContentForMagic ?? data) ||
|
|
2420
|
+
(isExplicitImageBlock ? 'image/png' : undefined)
|
|
2421
|
+
if (mimeType && mimeType.startsWith('image/')) {
|
|
2422
|
+
return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) }
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
return undefined
|
|
2428
|
+
}
|
|
1366
2429
|
export type ForwardPromptSelection =
|
|
1367
|
-
| {
|
|
2430
|
+
| {
|
|
2431
|
+
kind: 'prompt'
|
|
2432
|
+
text: string
|
|
2433
|
+
id?: string
|
|
2434
|
+
images?: PromptImageContent[]
|
|
2435
|
+
content?: PromptContentBlock[]
|
|
2436
|
+
}
|
|
1368
2437
|
| { kind: 'no-user-input' }
|
|
1369
2438
|
| { kind: 'empty' }
|
|
1370
2439
|
|
|
@@ -1381,28 +2450,72 @@ export type ForwardPromptSelection =
|
|
|
1381
2450
|
* (tests, headless callers) and counts as user input.
|
|
1382
2451
|
*
|
|
1383
2452
|
* @param messages - the request's ordered messages, exactly as the host built them.
|
|
1384
|
-
* @
|
|
2453
|
+
* @param context - optional host context or attachment resolver for reading attachments.
|
|
2454
|
+
* @returns the last real user message's text and any image content blocks, or why there is none to forward.
|
|
1385
2455
|
*/
|
|
1386
2456
|
export function selectForwardPrompt(messages: readonly {
|
|
1387
2457
|
role: string
|
|
1388
|
-
content?: Array<{ type?: string; text?: string }>
|
|
2458
|
+
content?: string | Array<{ type?: string; text?: string; [k: string]: unknown }> | { type?: string; text?: string; [k: string]: unknown }
|
|
1389
2459
|
source?: { kind?: string }
|
|
1390
2460
|
id?: string
|
|
1391
|
-
}[]): ForwardPromptSelection {
|
|
2461
|
+
}[], context?: unknown): ForwardPromptSelection {
|
|
1392
2462
|
let sawUserRole = false
|
|
1393
|
-
let last: {
|
|
2463
|
+
let last: {
|
|
2464
|
+
text: string
|
|
2465
|
+
id?: string
|
|
2466
|
+
images?: PromptImageContent[]
|
|
2467
|
+
content?: PromptContentBlock[]
|
|
2468
|
+
} | undefined
|
|
1394
2469
|
for (const message of messages) {
|
|
1395
2470
|
if (message.role !== 'user') continue
|
|
1396
2471
|
sawUserRole = true
|
|
1397
2472
|
const kind = message.source?.kind
|
|
1398
2473
|
if (kind !== undefined && kind !== 'user') continue
|
|
1399
|
-
|
|
2474
|
+
|
|
2475
|
+
const content = message.content ?? (message as { parts?: unknown }).parts
|
|
2476
|
+
const rawBlocks: Array<Record<string, unknown>> = Array.isArray(content)
|
|
2477
|
+
? (content as Array<Record<string, unknown>>)
|
|
2478
|
+
: (typeof content === 'string'
|
|
2479
|
+
? [{ type: 'text', text: content }]
|
|
2480
|
+
: (typeof content === 'object' && content !== null ? [content as Record<string, unknown>] : []))
|
|
2481
|
+
|
|
2482
|
+
const text = rawBlocks
|
|
1400
2483
|
.filter(block => block.type === 'text' && typeof block.text === 'string')
|
|
1401
2484
|
.map(block => block.text as string)
|
|
1402
2485
|
.join('\n')
|
|
1403
2486
|
.trim()
|
|
1404
|
-
|
|
1405
|
-
|
|
2487
|
+
|
|
2488
|
+
const images: PromptImageContent[] = []
|
|
2489
|
+
const contentBlocks: PromptContentBlock[] = []
|
|
2490
|
+
|
|
2491
|
+
for (const block of rawBlocks) {
|
|
2492
|
+
if (!block || typeof block !== 'object') continue
|
|
2493
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
2494
|
+
if (block.text.trim() !== '') {
|
|
2495
|
+
contentBlocks.push({ type: 'text', text: block.text })
|
|
2496
|
+
}
|
|
2497
|
+
} else {
|
|
2498
|
+
const resolved = resolveImageContent(block, context)
|
|
2499
|
+
if (resolved !== undefined) {
|
|
2500
|
+
images.push(resolved)
|
|
2501
|
+
contentBlocks.push(resolved)
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
if (text === '' && images.length === 0) continue
|
|
2507
|
+
|
|
2508
|
+
const item: {
|
|
2509
|
+
text: string
|
|
2510
|
+
id?: string
|
|
2511
|
+
images?: PromptImageContent[]
|
|
2512
|
+
content?: PromptContentBlock[]
|
|
2513
|
+
} = { text, ...(message.id === undefined ? {} : { id: message.id }) }
|
|
2514
|
+
if (images.length > 0) {
|
|
2515
|
+
item.images = images
|
|
2516
|
+
item.content = contentBlocks
|
|
2517
|
+
}
|
|
2518
|
+
last = item
|
|
1406
2519
|
}
|
|
1407
2520
|
if (last !== undefined) return { kind: 'prompt', ...last }
|
|
1408
2521
|
return sawUserRole ? { kind: 'no-user-input' } : { kind: 'empty' }
|
|
@@ -1552,7 +2665,34 @@ export function acpPresetStatusFromRoster(rows: readonly RosterPresetRow[] | und
|
|
|
1552
2665
|
|
|
1553
2666
|
// ---- Transcript ----
|
|
1554
2667
|
|
|
1555
|
-
/**
|
|
2668
|
+
/**
|
|
2669
|
+
* The kind of a state-only tick that consecutive repeats may share one entry.
|
|
2670
|
+
*
|
|
2671
|
+
* These are the frames the client folds to nothing (`STATE_ONLY_UPDATES`):
|
|
2672
|
+
* only their latest value is ever read. `session_info_update` qualifies only
|
|
2673
|
+
* while it carries no title — a title is content the replay must keep, so it
|
|
2674
|
+
* never lets a later tick replace its entry.
|
|
2675
|
+
*/
|
|
2676
|
+
function coalescibleStateKind(update: unknown): string | undefined {
|
|
2677
|
+
if (typeof update !== 'object' || update === null || Array.isArray(update)) return undefined
|
|
2678
|
+
const row = update as Record<string, unknown>
|
|
2679
|
+
const kind = row.sessionUpdate
|
|
2680
|
+
if (kind === 'usage_update' || kind === 'config_option_update') return kind
|
|
2681
|
+
if (kind !== 'session_info_update') return undefined
|
|
2682
|
+
const title = row.title
|
|
2683
|
+
return title === undefined || title === null || title === '' ? kind : undefined
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* Bounded log of raw ACP session updates with monotone seq.
|
|
2688
|
+
*
|
|
2689
|
+
* Consecutive ticks of the same state-only kind share one entry (the entry is
|
|
2690
|
+
* replaced in place, not appended): chatty agents emit a usage / phase tick
|
|
2691
|
+
* every few hundred milliseconds, and letting each occupy the ring evicts the
|
|
2692
|
+
* conversation itself. This costs nothing on the live path — `append` still
|
|
2693
|
+
* allocates a fresh seq and the bridge broadcasts that frame — but replay via
|
|
2694
|
+
* `since` only sees the newest tick of each run.
|
|
2695
|
+
*/
|
|
1556
2696
|
export class Transcript {
|
|
1557
2697
|
private readonly entries: Array<{ seq: number; update: unknown }> = []
|
|
1558
2698
|
private next = 1
|
|
@@ -1561,6 +2701,16 @@ export class Transcript {
|
|
|
1561
2701
|
|
|
1562
2702
|
append(update: unknown): number {
|
|
1563
2703
|
const seq = this.next++
|
|
2704
|
+
const last = this.entries[this.entries.length - 1]
|
|
2705
|
+
const kind = coalescibleStateKind(update)
|
|
2706
|
+
// Replace, don't push: `since(afterSeq)` keeps its filter, so intermediate
|
|
2707
|
+
// seq values are simply absent from replayed entries. Live SSE frames are
|
|
2708
|
+
// broadcast per append (the returned seq), and the client's live fold does
|
|
2709
|
+
// not depend on transcript contiguity — only `head` must keep advancing.
|
|
2710
|
+
if (kind !== undefined && last !== undefined && coalescibleStateKind(last.update) === kind) {
|
|
2711
|
+
this.entries[this.entries.length - 1] = { seq, update }
|
|
2712
|
+
return seq
|
|
2713
|
+
}
|
|
1564
2714
|
this.entries.push({ seq, update })
|
|
1565
2715
|
if (this.entries.length > this.max) this.entries.splice(0, this.entries.length - this.max)
|
|
1566
2716
|
return seq
|
|
@@ -1754,7 +2904,7 @@ export function permissionReplayFrames(pending: readonly PendingPermissionView[]
|
|
|
1754
2904
|
export interface AcpManager {
|
|
1755
2905
|
agentViews(): AgentView[]
|
|
1756
2906
|
startSession(agentId: string, cwd: string | undefined): Promise<{ sessionId: string }>
|
|
1757
|
-
prompt(sessionId: string,
|
|
2907
|
+
prompt(sessionId: string, prompt: string | PromptContentBlock[]): Promise<{ turnId: string }>
|
|
1758
2908
|
cancel(sessionId: string): Promise<void>
|
|
1759
2909
|
closeSession(sessionId: string): Promise<void>
|
|
1760
2910
|
setMode(sessionId: string, modeId: string): Promise<void>
|
|
@@ -1789,6 +2939,19 @@ export interface AcpManager {
|
|
|
1789
2939
|
* applies again. Applies to the running bridges without a restart.
|
|
1790
2940
|
*/
|
|
1791
2941
|
setApprovalDeadlines?(patch: ApprovalDeadlinePatch, reset?: boolean): Promise<void>
|
|
2942
|
+
/**
|
|
2943
|
+
* The idle-release window in force for chat processes, plus the built-in
|
|
2944
|
+
* default a reset returns to. `undefined` on a host half from before the
|
|
2945
|
+
* card existed, which hides the settings tab instead of showing a number
|
|
2946
|
+
* this browser invented.
|
|
2947
|
+
*/
|
|
2948
|
+
idleRelease?(): AcpIdleRelease | undefined
|
|
2949
|
+
/**
|
|
2950
|
+
* Write the idle-release window (in ms), or drop the persisted
|
|
2951
|
+
* override with `reset` so the built-in default applies again. Applies to
|
|
2952
|
+
* the running bridges without a restart.
|
|
2953
|
+
*/
|
|
2954
|
+
setIdleRelease?(idleTimeoutMs: number | undefined, reset?: boolean): Promise<void>
|
|
1792
2955
|
detectAgents?(): Promise<DetectedAgentInfo[]>
|
|
1793
2956
|
saveAgent?(def: AcpAgentDef): Promise<void>
|
|
1794
2957
|
deleteAgent?(agentId: string): Promise<void>
|
|
@@ -1861,6 +3024,21 @@ export interface AcpManager {
|
|
|
1861
3024
|
* without it the live (non-released) session is picked.
|
|
1862
3025
|
*/
|
|
1863
3026
|
setChatSessionMode?(dshSessionId: string, modeId: string, agentId?: string): Promise<void>
|
|
3027
|
+
/**
|
|
3028
|
+
* Bring one chat conversation's released session back on a fresh process on
|
|
3029
|
+
* the user's explicit gesture (`session/chat-resume`, the released-session
|
|
3030
|
+
* banner's button). Same recovery the next prompt performs lazily, minus
|
|
3031
|
+
* the create path; no-op when the session is already live.
|
|
3032
|
+
*/
|
|
3033
|
+
resumeChatSession?(dshSessionId: string): Promise<void>
|
|
3034
|
+
/**
|
|
3035
|
+
* Adopt the ACP side of a branched conversation (`session/chat-fork-adopt`):
|
|
3036
|
+
* the browser has already asked the HOST to fork the DSH session tree, and
|
|
3037
|
+
* this runs the agent's own `session/fork` of the parent's live ACP session,
|
|
3038
|
+
* then binds the child to it. All-or-nothing — nothing is bound on failure,
|
|
3039
|
+
* so the child stays an ordinary copied-history conversation.
|
|
3040
|
+
*/
|
|
3041
|
+
adoptForkedSession?(childDshSessionId: string, parentDshSessionId: string): Promise<{ sessionId: string; acpSessionId: string }>
|
|
1864
3042
|
/**
|
|
1865
3043
|
* Point one DSH chat conversation at an ACP agent — the plugin's own
|
|
1866
3044
|
* conversation↔agent mapping, never a DSH model route (see
|
|
@@ -1979,7 +3157,37 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
1979
3157
|
case 'session/start':
|
|
1980
3158
|
return ok(await manager.startSession(str(body, 'agentId', 64), optionalStr(body, 'cwd', 4096)))
|
|
1981
3159
|
case 'session/prompt': {
|
|
1982
|
-
|
|
3160
|
+
let promptInput: string | PromptContentBlock[]
|
|
3161
|
+
if (Array.isArray(body.prompt)) {
|
|
3162
|
+
if (body.prompt.length === 0) {
|
|
3163
|
+
throw new AcpError('bad_payload', 'acp: prompt array cannot be empty')
|
|
3164
|
+
}
|
|
3165
|
+
for (const b of body.prompt) {
|
|
3166
|
+
if (!b || typeof b !== 'object') throw new AcpError('bad_payload', 'acp: prompt blocks must be objects')
|
|
3167
|
+
const block = b as Record<string, unknown>
|
|
3168
|
+
if (block.type === 'text') {
|
|
3169
|
+
if (typeof block.text !== 'string') throw new AcpError('bad_payload', 'acp: text block requires string text')
|
|
3170
|
+
if (block.text.length > (1 << 20)) throw new AcpError('bad_payload', 'acp: text block exceeds max length')
|
|
3171
|
+
} else if (block.type === 'image') {
|
|
3172
|
+
if (typeof block.data !== 'string' || typeof block.mimeType !== 'string') {
|
|
3173
|
+
throw new AcpError('bad_payload', 'acp: image block requires string data and mimeType')
|
|
3174
|
+
}
|
|
3175
|
+
if (!block.mimeType.toLowerCase().startsWith('image/')) {
|
|
3176
|
+
throw new AcpError('bad_payload', 'acp: image block mimeType must start with image/')
|
|
3177
|
+
}
|
|
3178
|
+
if (block.uri !== undefined && typeof block.uri !== 'string') {
|
|
3179
|
+
throw new AcpError('bad_payload', 'acp: image block uri must be string')
|
|
3180
|
+
}
|
|
3181
|
+
if (block.data.length > (20 << 20)) throw new AcpError('bad_payload', 'acp: image block exceeds max data length')
|
|
3182
|
+
} else {
|
|
3183
|
+
throw new AcpError('bad_payload', `acp: unsupported prompt block type: ${String(block.type)}`)
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
promptInput = body.prompt as PromptContentBlock[]
|
|
3187
|
+
} else {
|
|
3188
|
+
promptInput = str(body, 'text', 1 << 20)
|
|
3189
|
+
}
|
|
3190
|
+
const { turnId } = await manager.prompt(str(body, 'sessionId', 64), promptInput)
|
|
1983
3191
|
return ok({ accepted: true as const, turnId })
|
|
1984
3192
|
}
|
|
1985
3193
|
case 'session/cancel':
|
|
@@ -2055,6 +3263,26 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
2055
3263
|
await manager.setChatSessionMode?.(dshSessionId, modeId, optionalStr(body, 'agentId', 64))
|
|
2056
3264
|
return ok({ done: true })
|
|
2057
3265
|
}
|
|
3266
|
+
case 'session/chat-resume': {
|
|
3267
|
+
// The released-session banner's explicit resume: the recovery the next
|
|
3268
|
+
// prompt performs lazily, on the user's gesture instead. Addressed by
|
|
3269
|
+
// DSH chat session id, like the rest of the conversation surfaces.
|
|
3270
|
+
const dshSessionId = str(body, 'dshSessionId', 128)
|
|
3271
|
+
await manager.resumeChatSession?.(dshSessionId)
|
|
3272
|
+
return ok({ done: true })
|
|
3273
|
+
}
|
|
3274
|
+
case 'session/chat-fork-adopt': {
|
|
3275
|
+
// The second half of a branched conversation: bind the DSH session the
|
|
3276
|
+
// host's `sessions.fork` just created to the agent's own `session/fork`
|
|
3277
|
+
// of the parent's live ACP session. Both ids are validated before the
|
|
3278
|
+
// optional call — the same shape rule as `session/chat-list` above.
|
|
3279
|
+
// The adopted ids ride the answer (the protocol's ChatForkAdoptResult);
|
|
3280
|
+
// a manager without the method keeps the plain `done` shape.
|
|
3281
|
+
const dshSessionId = str(body, 'dshSessionId', 128)
|
|
3282
|
+
const parentDshSessionId = str(body, 'parentDshSessionId', 128)
|
|
3283
|
+
const adopted = await manager.adoptForkedSession?.(dshSessionId, parentDshSessionId)
|
|
3284
|
+
return ok(adopted ?? { done: true })
|
|
3285
|
+
}
|
|
2058
3286
|
case 'session/chat-bind': {
|
|
2059
3287
|
// Point one chat conversation at an ACP agent. The plugin's own
|
|
2060
3288
|
// conversation↔agent mapping — NOT a DSH model route (see
|
|
@@ -2141,6 +3369,16 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
2141
3369
|
await manager.setApprovalDeadlines?.(patch, body.reset === true)
|
|
2142
3370
|
return ok({ deadlines: manager.approvalDeadlines?.() })
|
|
2143
3371
|
}
|
|
3372
|
+
case 'ui/idle-release':
|
|
3373
|
+
// Absent on a host half from before the tab existed: the browser then
|
|
3374
|
+
// renders nothing rather than a number it cannot trust.
|
|
3375
|
+
return ok({ idleRelease: manager.idleRelease?.() })
|
|
3376
|
+
case 'ui/idle-release-set': {
|
|
3377
|
+
const reset = body.reset === true
|
|
3378
|
+
const idleTimeoutMs = reset ? undefined : validateIdleReleaseMs(body.idleTimeoutMs, 'bad_payload')
|
|
3379
|
+
await manager.setIdleRelease?.(idleTimeoutMs, reset)
|
|
3380
|
+
return ok({ idleRelease: manager.idleRelease?.() })
|
|
3381
|
+
}
|
|
2144
3382
|
case 'agent/set-exposed-models': {
|
|
2145
3383
|
const agentId = str(body, 'agentId', 64)
|
|
2146
3384
|
const raw = body.exposedModels
|