feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -28,128 +28,130 @@ function countGifFrameBlocks(buffer) {
return count
}
function readAssetFrames(assetPath, options = {}) {
const metadata = execFileSync('identify', ['-format', '%w %h %n\n', assetPath], {
encoding: 'utf8'
}).trim().split('\n')[0].split(/\s+/).map(Number)
const sourceWidth = metadata[0]
const sourceHeight = metadata[1]
const frameCount = metadata[2] || 1
const width = options.width || sourceWidth
const height = options.height || sourceHeight
const convertArgs = [assetPath, '-coalesce']
if (options.width || options.height) {
convertArgs.push('-resize', `${width}x${height}!`)
}
convertArgs.push('-alpha', 'off', '-depth', '8', 'rgb:-')
const pixels = execFileSync('convert', convertArgs, {
maxBuffer: 64 * 1024 * 1024
})
const frameSize = width * height * 3
return {
frameCount: Math.min(frameCount, Math.floor(pixels.length / frameSize)),
frameSize,
height,
pixels,
width
}
}
function measureGifMotion(assetPath) {
const script = `
from PIL import Image, ImageSequence
import json
import sys
image = Image.open(sys.argv[1])
frames = [frame.convert("RGB").resize((64, 64)) for frame in ImageSequence.Iterator(image)]
def delta(left, right):
left_pixels = left.load()
right_pixels = right.load()
total = 0
for y in range(64):
for x in range(64):
a = left_pixels[x, y]
b = right_pixels[x, y]
total += abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2])
return total / (64 * 64 * 3)
adjacent = [delta(frames[index], frames[index + 1]) for index in range(len(frames) - 1)]
adjacent_sorted = sorted(adjacent)
median = adjacent_sorted[len(adjacent_sorted) // 2]
print(json.dumps({
"medianAdjacentDelta": median,
"seamDelta": delta(frames[-1], frames[0])
}))
`
return JSON.parse(execFileSync('python3', ['-', assetPath], {
encoding: 'utf8',
input: script
}).trim())
const { frameCount, frameSize, pixels } = readAssetFrames(assetPath, { width: 64, height: 64 })
const delta = (leftFrame, rightFrame) => {
const leftOffset = leftFrame * frameSize
const rightOffset = rightFrame * frameSize
let total = 0
for (let index = 0; index < frameSize; index += 1) {
total += Math.abs(pixels[leftOffset + index] - pixels[rightOffset + index])
}
return total / frameSize
}
const adjacent = Array.from(
{ length: Math.max(0, frameCount - 1) },
(_, index) => delta(index, index + 1)
).sort((left, right) => left - right)
return {
medianAdjacentDelta: adjacent[Math.floor(adjacent.length / 2)] || 0,
seamDelta: delta(frameCount - 1, 0)
}
}
function measureGifDuration(assetPath) {
const script = `
from PIL import Image
import sys
image = Image.open(sys.argv[1])
total = 0
for index in range(getattr(image, "n_frames", 1)):
image.seek(index)
total += image.info.get("duration", 0)
print(total)
`
return Number(execFileSync('python3', ['-', assetPath], {
encoding: 'utf8',
input: script
}).trim())
const delays = execFileSync('identify', ['-format', '%T\n', assetPath], {
encoding: 'utf8'
}).trim().split('\n').map(Number)
return delays.reduce((total, delay) => total + delay * 10, 0)
}
function measureOrbAssetPresentation(assetPath) {
const script = `
from PIL import Image
import json
import sys
const { frameCount, frameSize, height, pixels, width } = readAssetFrames(assetPath)
let minimumCornerLuma = 255
let maximumCornerLuma = 0
let minimumBackgroundSimilarityRatio = 1
let minimumForegroundWidthRatio = 1
let minimumForegroundHeightRatio = 1
image = Image.open(sys.argv[1])
frame_count = getattr(image, "n_frames", 1)
width, height = image.size
minimum_corner_luma = 255
maximum_corner_luma = 0
minimum_background_similarity_ratio = 1
minimum_foreground_width_ratio = 1
minimum_foreground_height_ratio = 1
for index in range(frame_count):
if frame_count > 1:
image.seek(index)
rgb = image.convert("RGB")
corners = [
rgb.getpixel((0, 0)),
rgb.getpixel((width - 1, 0)),
rgb.getpixel((0, height - 1)),
rgb.getpixel((width - 1, height - 1)),
for (let frame = 0; frame < frameCount; frame += 1) {
const frameOffset = frame * frameSize
const pixelAt = (x, y) => {
const offset = frameOffset + (y * width + x) * 3
return [pixels[offset], pixels[offset + 1], pixels[offset + 2]]
}
const corners = [
pixelAt(0, 0),
pixelAt(width - 1, 0),
pixelAt(0, height - 1),
pixelAt(width - 1, height - 1)
]
corner_lumas = [sum(pixel) / 3 for pixel in corners]
minimum_corner_luma = min(minimum_corner_luma, min(corner_lumas))
maximum_corner_luma = max(maximum_corner_luma, max(corner_lumas))
background = tuple(round(sum(pixel[channel] for pixel in corners) / len(corners)) for channel in range(3))
foreground_mask = Image.new("L", (width, height), 0)
foreground_pixels = foreground_mask.load()
background_similarity = 0
rgb_pixels = rgb.load()
for y in range(height):
for x in range(width):
pixel = rgb_pixels[x, y]
diff = sum(abs(pixel[channel] - background[channel]) for channel in range(3))
if diff > 22:
foreground_pixels[x, y] = 255
if diff <= 12:
background_similarity += 1
foreground_box = foreground_mask.getbbox()
if foreground_box:
minimum_foreground_width_ratio = min(
minimum_foreground_width_ratio,
(foreground_box[2] - foreground_box[0]) / width
)
minimum_foreground_height_ratio = min(
minimum_foreground_height_ratio,
(foreground_box[3] - foreground_box[1]) / height
)
minimum_background_similarity_ratio = min(
minimum_background_similarity_ratio,
background_similarity / (width * height)
)
const cornerLumas = corners.map((pixel) => (pixel[0] + pixel[1] + pixel[2]) / 3)
minimumCornerLuma = Math.min(minimumCornerLuma, ...cornerLumas)
maximumCornerLuma = Math.max(maximumCornerLuma, ...cornerLumas)
const background = [0, 1, 2].map((channel) => Math.round(
corners.reduce((total, pixel) => total + pixel[channel], 0) / corners.length
))
let backgroundSimilarity = 0
let minX = width
let minY = height
let maxX = -1
let maxY = -1
print(json.dumps({
"minimumCornerLuma": minimum_corner_luma,
"maximumCornerLuma": maximum_corner_luma,
"minimumBackgroundSimilarityRatio": minimum_background_similarity_ratio,
"minimumForegroundWidthRatio": minimum_foreground_width_ratio,
"minimumForegroundHeightRatio": minimum_foreground_height_ratio,
"width": width,
"height": height
}))
`
return JSON.parse(execFileSync('python3', ['-', assetPath], {
encoding: 'utf8',
input: script
}).trim())
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const pixel = pixelAt(x, y)
const diff = pixel.reduce(
(total, value, channel) => total + Math.abs(value - background[channel]),
0
)
if (diff > 22) {
minX = Math.min(minX, x)
minY = Math.min(minY, y)
maxX = Math.max(maxX, x)
maxY = Math.max(maxY, y)
}
if (diff <= 12) {
backgroundSimilarity += 1
}
}
}
if (maxX >= minX && maxY >= minY) {
minimumForegroundWidthRatio = Math.min(minimumForegroundWidthRatio, (maxX - minX + 1) / width)
minimumForegroundHeightRatio = Math.min(minimumForegroundHeightRatio, (maxY - minY + 1) / height)
}
minimumBackgroundSimilarityRatio = Math.min(
minimumBackgroundSimilarityRatio,
backgroundSimilarity / (width * height)
)
}
return {
minimumCornerLuma,
maximumCornerLuma,
minimumBackgroundSimilarityRatio,
minimumForegroundWidthRatio,
minimumForegroundHeightRatio,
width,
height
}
}
const appShell = readSource('../src/views/AppShellRouteView.vue')
@@ -230,7 +232,7 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeSurface, /费用测算中,请稍等/)
assert.match(aiModeSurface, /rows="3"/)
assert.match(aiModeSurface, /workbench-ai-composer-toolbar/)
assert.match(aiModeSurface, /<article v-for="file in runtime\.selectedFileCards"[\s\S]*class="workbench-ai-file-card"/)
assert.match(aiModeSurface, /<article[\s\S]{0,140}v-for="file in runtime\.selectedFileCards"[\s\S]{0,140}class="workbench-ai-file-card"/)
assert.match(aiModeSurface, /class="workbench-ai-file-card__ocr"/)
assert.match(aiModeSurface, /file\.ocrState\?\.label/)
assert.match(aiModeSurface, /mdi mdi-text-recognition/)
@@ -239,7 +241,7 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeSurface, /:aria-label="`移除附件 \$\{file\.name\}`"/)
assert.match(aiModeSurface, /function removeAiModeFile\(fileKey\)/)
assert.match(aiModeSurface, /const selectedFileCards = computed/)
assert.match(aiModeSurface, /resolveAiComposerFileType\(file\)/)
assert.match(aiModeSurface, /resolveAiComposerFileType\(file, previewAsset\)/)
assert.match(aiModeSurface, /AI_COMPOSER_FILE_TYPE_META = \{[\s\S]*pdf:\s*\{ label:\s*'PDF'/)
assert.match(aiModeSurface, /buildFileIdentity,[\s\S]*collectReceiptFiles[\s\S]*travelReimbursementAttachmentModel\.js/)
assert.match(aiModeSurface, /MAX_ATTACHMENTS,[\s\S]*mergeFilesWithLimit[\s\S]*travelReimbursementAttachmentModel\.js/)
@@ -345,7 +347,8 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary__scope\)/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card-list\) \{[\s\S]*gap:\s*16px;/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\) \{[\s\S]*url\("\.\.\/\.\.\/ai-document-card-bg\.png"\);/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\) \{[\s\S]*background-color:\s*#ffffff;/)
assert.doesNotMatch(aiModeStyles, /ai-document-card-bg\.png/)
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)::before/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: var\(--ai-document-card-head-bg\);/)
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\.is-success \.ai-document-card__head\)/)
@@ -422,7 +425,8 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeStyles, /\.workbench-ai-mode\s*\{[\s\S]*min-height:\s*100%;[\s\S]*background:/)
assert.match(aiModeStyles, /\.workbench-ai-mode\.has-conversation\s*\{[\s\S]*place-items:\s*stretch;[\s\S]*padding:\s*0;/)
assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*border-radius:\s*20px;[\s\S]*box-shadow:/)
assert.match(fileStripRule, /flex-wrap:\s*wrap;/)
assert.match(fileStripRule, /flex-wrap:\s*nowrap;/)
assert.match(fileStripRule, /overflow-x:\s*auto;/)
assert.match(fileStripRule, /justify-content:\s*flex-start;/)
assert.match(fileCardRule, /grid-template-columns:\s*48px minmax\(0,\s*1fr\) 30px;/)
assert.match(fileCardRule, /border-radius:\s*16px;/)
@@ -579,7 +583,7 @@ test('AI mode normal assistant requests include OCR context for uploaded receipt
assert.match(aiModeSurface, /function buildAiModeReceiptContextCacheKey\(ocrFiles = \[\]\)/)
assert.match(aiModeSurface, /applyAiModeReceiptRecognitionResult\(ocrFiles, context\)/)
assert.match(aiModeSurface, /buildFileIdentity\(file\)/)
assert.match(aiModeSurface, /watch\(selectedFiles, \(files\) => \{[\s\S]*attachmentFlow\.primeAiModeReceiptContext\(files\)/)
assert.match(aiModeSurface, /watch\(selectedFiles, \(files(?:, previousFiles = \[\])?\) => \{[\s\S]*attachmentFlow\.primeAiModeReceiptContext\(files\)/)
assert.match(aiModeSurface, /async function collectAiModeReceiptContext\(files = \[\]\)/)
assert.match(aiModeSurface, /cached\?\.status === 'pending'[\s\S]*await cached\.promise/)
assert.match(aiModeSurface, /collectReceiptFiles\(\{[\s\S]*files:\s*ocrFiles,[\s\S]*recognizeOcrFiles[\s\S]*\}\)/)