2026-06-18 22:12:24 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
|
|
|
import { readFileSync, statSync } from 'node:fs'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
|
|
|
|
function readSource(path) {
|
|
|
|
|
try {
|
|
|
|
|
return readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
|
|
|
} catch {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readRuleBody(source, selector) {
|
|
|
|
|
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
const match = source.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`))
|
|
|
|
|
return match?.[1] || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countGifFrameBlocks(buffer) {
|
|
|
|
|
let count = 0
|
|
|
|
|
for (let index = 0; index < buffer.length - 2; index += 1) {
|
|
|
|
|
if (buffer[index] === 0x21 && buffer[index + 1] === 0xf9 && buffer[index + 2] === 0x04) {
|
|
|
|
|
count += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function measureOrbAssetPresentation(assetPath) {
|
|
|
|
|
const script = `
|
|
|
|
|
from PIL import Image
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
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)),
|
|
|
|
|
]
|
|
|
|
|
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)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const appShell = readSource('../src/views/AppShellRouteView.vue')
|
|
|
|
|
const workbenchView = readSource('../src/views/PersonalWorkbenchView.vue')
|
|
|
|
|
const aiMode = readSource('../src/components/business/PersonalWorkbenchAiMode.vue')
|
|
|
|
|
const aiModeStyles = readSource('../src/assets/styles/components/personal-workbench-ai-mode.css')
|
|
|
|
|
const workbenchViewStyles = readSource('../src/assets/styles/views/personal-workbench-view.css')
|
|
|
|
|
const appStyles = readSource('../src/assets/styles/app.css')
|
|
|
|
|
const aiBackgroundRule = readRuleBody(aiModeStyles, '.workbench-ai-mode::after')
|
|
|
|
|
const orbRule = readRuleBody(aiModeStyles, '.workbench-ai-orb')
|
|
|
|
|
const orbImageRule = readRuleBody(aiModeStyles, '.workbench-ai-orb__image')
|
|
|
|
|
const composerRule = readRuleBody(aiModeStyles, '.workbench-ai-composer')
|
|
|
|
|
const composerTextareaRule = readRuleBody(aiModeStyles, '.workbench-ai-composer textarea')
|
|
|
|
|
const orbIconAsset = fileURLToPath(
|
|
|
|
|
new URL('../src/assets/workbench-ai-mode-orb-icon.gif', import.meta.url)
|
|
|
|
|
)
|
|
|
|
|
const orbIconPngAsset = fileURLToPath(
|
|
|
|
|
new URL('../src/assets/workbench-ai-mode-orb-icon.png', import.meta.url)
|
|
|
|
|
)
|
|
|
|
|
const orbIconBuffer = readFileSync(orbIconAsset)
|
|
|
|
|
|
|
|
|
|
test('app shell owns the workbench mode and wires it through topbar and content', () => {
|
|
|
|
|
assert.match(appShell, /const workbenchMode = ref\('traditional'\)/)
|
|
|
|
|
assert.match(appShell, /function toggleWorkbenchMode\(\)/)
|
|
|
|
|
assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
|
|
|
|
|
assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/)
|
|
|
|
|
assert.match(appShell, /workbenchMode\.value = nextMode/)
|
|
|
|
|
assert.match(appShell, /sidebarCollapsed\.value = sidebarCollapsedBeforeAiMode\.value/)
|
|
|
|
|
assert.match(appShell, /<TopBar[\s\S]*:workbench-mode="workbenchMode"[\s\S]*@toggle-workbench-mode="toggleWorkbenchMode"/)
|
|
|
|
|
assert.match(appShell, /<PersonalWorkbenchView[\s\S]*:workbench-mode="workbenchMode"/)
|
|
|
|
|
assert.match(appShell, /const isAiShellMode = computed\(\(\) => workbenchMode\.value === 'ai'\)/)
|
|
|
|
|
assert.match(appShell, /const isWorkbenchAiMode = computed\(\(\) => activeView\.value === 'workbench' && workbenchMode\.value === 'ai'\)/)
|
|
|
|
|
assert.match(appShell, /'workbench-ai-sidebar-active': isAiShellMode/)
|
|
|
|
|
assert.match(appShell, /'workbench-workarea-ai-mode': isWorkbenchAiMode/)
|
|
|
|
|
assert.match(appStyles, /\.workarea\.workbench-workarea\.workbench-workarea-ai-mode\s*\{[\s\S]*padding:\s*0;[\s\S]*background:\s*transparent;/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('personal workbench view swaps the traditional dashboard with the AI mode screen', () => {
|
|
|
|
|
assert.match(workbenchView, /import PersonalWorkbenchAiMode from '\.\.\/components\/business\/PersonalWorkbenchAiMode\.vue'/)
|
|
|
|
|
assert.match(workbenchView, /<Transition[\s\S]*name="workbench-mode-fade"[\s\S]*mode="out-in"/)
|
|
|
|
|
assert.match(workbenchView, /<PersonalWorkbenchAiMode[\s\S]*v-if="workbenchMode === 'ai'"[\s\S]*key="ai"/)
|
|
|
|
|
assert.match(workbenchView, /:sidebar-command="aiSidebarCommand"/)
|
|
|
|
|
assert.match(workbenchView, /@conversation-change="emit\('ai-conversation-change', \$event\)"/)
|
|
|
|
|
assert.match(workbenchView, /@conversation-history-change="emit\('ai-conversation-history-change', \$event\)"/)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(workbenchView, /@open-document="emit\('open-document', \$event\)"/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(workbenchView, /<PersonalWorkbench[\s\S]*v-else[\s\S]*key="traditional"/)
|
|
|
|
|
assert.match(workbenchView, /workbenchMode:\s*\{[\s\S]*type:\s*String,[\s\S]*default:\s*'traditional'/)
|
|
|
|
|
assert.match(workbenchView, /aiSidebarCommand:\s*\{[\s\S]*type:\s*Object/)
|
|
|
|
|
assert.match(workbenchView, /personal-workbench-view\.css/)
|
|
|
|
|
assert.match(workbenchViewStyles, /\.workbench-mode-fade-enter-active,[\s\S]*\.workbench-mode-fade-leave-active\s*\{[\s\S]*transition:/)
|
|
|
|
|
assert.match(workbenchViewStyles, /\.workbench-mode-fade-enter-from,[\s\S]*\.workbench-mode-fade-leave-to\s*\{[\s\S]*opacity:\s*0;[\s\S]*transform:\s*translateY\(10px\) scale\(0\.992\);/)
|
|
|
|
|
assert.match(workbenchViewStyles, /@media \(prefers-reduced-motion:\s*reduce\)/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('AI mode screen follows the approved reference structure', () => {
|
|
|
|
|
assert.match(aiMode, /personal-workbench-ai-mode\.css/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /workbench-ai-mode-robot-bg\.png/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-mode-orb-icon\.gif/)
|
|
|
|
|
assert.match(aiMode, /<img[\s\S]*class="workbench-ai-orb__image"/)
|
|
|
|
|
assert.match(aiMode, /小财管家/)
|
|
|
|
|
assert.match(aiMode, /我是您的小财管家/)
|
2026-06-20 14:42:04 +08:00
|
|
|
assert.match(aiMode, /今天我能帮您做点什么?/)
|
|
|
|
|
assert.match(aiMode, /费用测算中,请稍等/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiMode, /rows="3"/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-composer-toolbar/)
|
|
|
|
|
assert.match(aiMode, /Axiom Ultra 3\.1/)
|
|
|
|
|
assert.match(aiMode, /mdi mdi-calendar-range/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-date-popover/)
|
|
|
|
|
assert.match(aiMode, /type="date"/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /mdi mdi-web/)
|
|
|
|
|
assert.match(aiMode, /mdi mdi-microphone-outline/)
|
|
|
|
|
assert.match(aiMode, /mdi mdi-arrow-up/)
|
|
|
|
|
assert.match(aiMode, /快速开始/)
|
|
|
|
|
assert.match(aiMode, /action-icon-wrapper/)
|
|
|
|
|
assert.match(aiMode, /发起报销/)
|
|
|
|
|
assert.match(aiMode, /查询预算/)
|
|
|
|
|
assert.match(aiMode, /解释制度/)
|
|
|
|
|
assert.match(aiMode, /催办审批/)
|
|
|
|
|
assert.match(aiMode, /<Transition name="workbench-ai-panel-swap" mode="out-in" appear>/)
|
|
|
|
|
assert.match(aiMode, /@submit\.prevent="submitAiModePrompt"/)
|
|
|
|
|
assert.equal((aiMode.match(/type="submit"[\s\S]{0,160}class="workbench-ai-send-btn"/g) || []).length, 2)
|
|
|
|
|
assert.match(aiMode, /class="workbench-ai-conversation"/)
|
|
|
|
|
assert.match(aiMode, /class="workbench-ai-thread"[\s\S]*@scroll\.passive="handleInlineConversationScroll"/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-answer-card/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-answer-markdown/)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiMode, /v-html="renderInlineConversationHtml\(message\.content\)"/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiMode, /workbench-ai-message-actions/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-conversation-actions/)
|
|
|
|
|
assert.match(aiMode, /scrollInlineConversationToTop/)
|
|
|
|
|
assert.match(aiMode, /requestDeleteCurrentConversation/)
|
|
|
|
|
assert.match(aiMode, /confirmDeleteConversation/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-confirm-dialog/)
|
|
|
|
|
assert.match(aiMode, /workbench-ai-thinking-toggle/)
|
|
|
|
|
assert.match(aiMode, /小财业务思考/)
|
|
|
|
|
assert.match(aiMode, /class="workbench-ai-thinking-expanded"/)
|
|
|
|
|
assert.match(aiMode, /class="workbench-ai-thinking-collapse-btn"/)
|
|
|
|
|
assert.match(aiMode, /class="workbench-ai-thinking-collapse-btn"[\s\S]*@click="toggleInlineThinking\(message\)"/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /:disabled="message\.pending"/)
|
|
|
|
|
assert.match(aiMode, /isInlineThinkingExpanded/)
|
|
|
|
|
assert.match(aiMode, /toggleInlineThinking/)
|
|
|
|
|
assert.match(aiMode, /const thinkingCollapsedMessageIds = ref\(new Set\(\)\)/)
|
|
|
|
|
assert.match(aiMode, /thinkingCollapsedMessageIds\.value\.has\(message\.id\)/)
|
|
|
|
|
assert.match(aiMode, /nextCollapsedIds\.add\(message\.id\)/)
|
|
|
|
|
assert.match(aiMode, /nextCollapsedIds\.delete\(message\.id\)/)
|
|
|
|
|
assert.match(aiMode, /message\.pending && !hasInlineThinking\(message\)/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /小财管家正在思考/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /思考过程/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /message\.pending \?/)
|
2026-06-20 14:42:04 +08:00
|
|
|
assert.match(aiMode, /继续和小财管家对话\.\.\./)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__foot\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__meta\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
2026-06-20 21:44:16 +08:00
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-list\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-item\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-meta\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)/)
|
|
|
|
|
assert.match(
|
|
|
|
|
aiModeStyles,
|
|
|
|
|
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-item\)\s*\{[\s\S]*grid-template-columns:\s*minmax\(220px,\s*1\.15fr\)\s*minmax\(260px,\s*0\.85fr\)\s*auto;/
|
|
|
|
|
)
|
|
|
|
|
assert.match(
|
|
|
|
|
aiModeStyles,
|
|
|
|
|
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)[\s\S]*background:\s*#2563eb;/
|
|
|
|
|
)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiMode, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
|
|
|
|
assert.match(aiMode, /import \{ fetchStewardPlan, fetchStewardPlanStream \} from '\.\.\/\.\.\/services\/steward\.js'/)
|
|
|
|
|
assert.match(aiMode, /import \{ useWorkbenchComposerDate \} from '\.\.\/\.\.\/composables\/useWorkbenchComposerDate\.js'/)
|
|
|
|
|
assert.match(aiMode, /loadAiWorkbenchConversationHistory/)
|
|
|
|
|
assert.match(aiMode, /saveAiWorkbenchConversation/)
|
|
|
|
|
assert.match(aiMode, /deleteAiWorkbenchConversation/)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiMode, /import \{ renderAiConversationHtml \} from '\.\.\/\.\.\/utils\/aiConversationHtmlRenderer\.js'/)
|
|
|
|
|
assert.match(aiMode, /function renderInlineConversationHtml\(content\) \{[\s\S]*return renderAiConversationHtml\(content\)[\s\S]*\}/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /import \{ renderMarkdown \} from '\.\.\/\.\.\/utils\/markdown\.js'/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiMode, /buildStewardPlanRequest/)
|
|
|
|
|
assert.match(aiMode, /buildStewardPlanMessageText/)
|
|
|
|
|
assert.match(aiMode, /buildStewardSuggestedActions/)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiMode, /const emit = defineEmits\(\['conversation-change', 'conversation-history-change', 'open-document'\]\)/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiMode, /function startInlineConversation\(prompt, entry = \{\}, files = \[\]\)/)
|
|
|
|
|
assert.match(aiMode, /activateInlineConversation\(\{[\s\S]*title:[\s\S]*\}\)[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user'/)
|
|
|
|
|
assert.match(aiMode, /persistCurrentConversation\(\)/)
|
|
|
|
|
assert.match(aiMode, /refreshConversationHistory\(\)/)
|
|
|
|
|
assert.match(aiMode, /fetchStewardPlanStream\(/)
|
|
|
|
|
assert.match(aiMode, /fetchStewardPlan\(/)
|
|
|
|
|
assert.match(aiMode, /const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6/)
|
|
|
|
|
assert.match(aiMode, /function updateInlineMessageContent\(message, content\)/)
|
|
|
|
|
assert.match(aiMode, /async function streamInlineAssistantContent\(messageId, content\)/)
|
|
|
|
|
assert.match(aiMode, /const requiredApplicationContinuationFlow = resolveRequiredApplicationGateContinuationFlow\(normalizedPlan\)/)
|
|
|
|
|
assert.match(aiMode, /const finalMessageText = requiredApplicationContinuationFlow[\s\S]*buildAiRequiredApplicationGateAutoMessage\(normalizedPlan, requiredApplicationContinuationFlow\)[\s\S]*buildStewardPlanMessageText\(plan\)/)
|
|
|
|
|
assert.match(aiMode, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
|
|
|
|
|
assert.match(aiMode, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
|
2026-06-20 14:42:04 +08:00
|
|
|
assert.match(aiMode, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
|
|
|
|
|
assert.match(aiMode, /需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /\*\*申请单号:\*\*/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.doesNotMatch(aiMode, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /runOrchestrator\(/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /buildFallbackAnswer/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /已使用本地回复/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /emit\('open-assistant'/)
|
|
|
|
|
assert.match(aiModeStyles, /--ai-theme-rgb:\s*var\(--theme-primary-rgb/)
|
|
|
|
|
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(composerRule, /min-height:\s*154px;/)
|
|
|
|
|
assert.match(composerRule, /grid-template-rows:\s*minmax\(80px,\s*1fr\) auto;/)
|
|
|
|
|
assert.match(composerTextareaRule, /min-height:\s*80px;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /--workbench-ai-robot-image/)
|
|
|
|
|
assert.match(aiBackgroundRule, /inset:\s*0;/)
|
|
|
|
|
assert.match(aiBackgroundRule, /linear-gradient\(90deg,\s*rgba\(var\(--ai-theme-rgb\)/)
|
|
|
|
|
assert.match(aiBackgroundRule, /background-size:\s*56px 56px,\s*56px 56px,\s*auto,\s*auto;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-orb\s*\{[\s\S]*border-radius:\s*50%;/)
|
|
|
|
|
assert.match(orbRule, /rgba\(255,\s*255,\s*255,\s*0\.98\)/)
|
|
|
|
|
assert.match(orbRule, /rgba\(47,\s*124,\s*255,\s*0\.18\)/)
|
|
|
|
|
assert.match(orbRule, /width:\s*clamp\(118px,\s*8vw,\s*132px\);/)
|
|
|
|
|
assert.match(orbRule, /height:\s*clamp\(118px,\s*8vw,\s*132px\);/)
|
|
|
|
|
assert.match(orbRule, /animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(orbImageRule, /width:\s*100%;/)
|
|
|
|
|
assert.match(orbImageRule, /height:\s*100%;/)
|
|
|
|
|
assert.match(orbImageRule, /object-fit:\s*contain;/)
|
|
|
|
|
assert.match(orbImageRule, /object-position:\s*center center;/)
|
|
|
|
|
assert.doesNotMatch(orbImageRule, /transform:/)
|
|
|
|
|
assert.match(aiModeStyles, /@keyframes workbenchAiControlIn\s*\{[\s\S]*opacity:\s*0;[\s\S]*translateY\(18px\)[\s\S]*opacity:\s*1;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-copy\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-composer\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-composer textarea\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-icon-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-send-btn\s*\{[\s\S]*animation:\s*workbenchAiControlIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-action:nth-child\(4\)\s*\{[\s\S]*animation-delay:\s*520ms;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-conversation\s*\{[\s\S]*grid-template-rows:\s*minmax\(0,\s*1fr\) auto;/)
|
|
|
|
|
assert.match(aiMode, /const inlineConversationAutoScrollPinned = ref\(true\)/)
|
|
|
|
|
assert.match(aiMode, /const INLINE_AUTO_SCROLL_THRESHOLD = 96/)
|
|
|
|
|
assert.match(aiMode, /const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260/)
|
|
|
|
|
assert.match(aiMode, /function isInlineConversationNearBottom\(\)/)
|
|
|
|
|
assert.match(aiMode, /function handleInlineConversationScroll\(\)\s*\{[\s\S]*inlineConversationAutoScrollPinned\.value = isInlineConversationNearBottom\(\)[\s\S]*\}/)
|
|
|
|
|
assert.match(aiMode, /function forceInlineConversationToBottom\(\)/)
|
|
|
|
|
assert.match(aiMode, /el\.scrollTop = el\.scrollHeight/)
|
|
|
|
|
assert.match(aiMode, /function scrollInlineConversationToBottom\(options = \{\}\)/)
|
|
|
|
|
assert.match(aiMode, /const shouldScroll = options\.force !== false/)
|
|
|
|
|
assert.match(aiMode, /if \(!shouldScroll\) \{[\s\S]*return[\s\S]*\}/)
|
|
|
|
|
assert.match(aiMode, /window\.requestAnimationFrame\(\(\) => \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}\)/)
|
|
|
|
|
assert.match(aiMode, /window\.setTimeout\(\(\) => \{[\s\S]*if \(inlineConversationAutoScrollPinned\.value\) \{[\s\S]*forceInlineConversationToBottom\(\)[\s\S]*\}[\s\S]*\}, INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS\)/)
|
|
|
|
|
assert.match(aiMode, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*updateInlineMessageContent\(message, streamedContent\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/)
|
|
|
|
|
assert.match(aiMode, /const shouldAutoScroll = inlineConversationAutoScrollPinned\.value[\s\S]*appendInlineMessageContent\(message, data\.delta \|\| data\.content \|\| data\.text \|\| ''\)[\s\S]*scrollInlineConversationToBottom\(\{ force: shouldAutoScroll \}\)/)
|
|
|
|
|
assert.match(aiMode, /inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value\.push\(createInlineMessage\('user', cleanPrompt\)\)/)
|
|
|
|
|
assert.match(aiMode, /function openInlineRecentConversation\(item = \{\}\) \{[\s\S]*inlineConversationAutoScrollPinned\.value = true[\s\S]*conversationMessages\.value =/)
|
|
|
|
|
assert.doesNotMatch(aiMode, /scrollTo\(\{ top: el\.scrollHeight, behavior: 'smooth' \}\)/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-direction:\s*column;[\s\S]*overflow-y:\s*auto;[\s\S]*scrollbar-width:\s*none;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thread\s*>\s*:first-child\s*\{[\s\S]*margin-top:\s*auto;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-message\s*\{[\s\S]*flex:\s*0 0 auto;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-empty-thread\s*\{[\s\S]*flex:\s*0 0 auto;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /align-content:\s*end;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thread\s*\{[\s\S]*scroll-behavior:\s*smooth;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thread::-webkit-scrollbar\s*\{[\s\S]*display:\s*none;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*relative;[\s\S]*z-index:\s*6;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*position:\s*sticky;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-conversation-bottom\s*\{[\s\S]*bottom:\s*0;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-conversation-bottom::before\s*\{[\s\S]*display:\s*none;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-panel\s*\{[\s\S]*display:\s*grid;[\s\S]*border:\s*1px solid rgba\(191,\s*219,\s*254,\s*0\.58\);/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-toggle\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-list\s*\{[\s\S]*border:\s*0;[\s\S]*background:\s*transparent;[\s\S]*overflow:\s*visible;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-item\s*\{[\s\S]*grid-template-columns:\s*18px minmax\(0,\s*1fr\);/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-dot\s*\{[\s\S]*justify-self:\s*center;/)
|
|
|
|
|
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-thinking-collapse-btn:disabled/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-thinking-collapse-enter-active,[\s\S]*\.workbench-ai-thinking-collapse-leave-active\s*\{[\s\S]*max-height 220ms ease/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-confirm-dialog\s*\{[\s\S]*border-radius:\s*18px;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-card\s*\{[\s\S]*box-shadow:\s*none;[\s\S]*backdrop-filter:\s*none;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown\s*\{[\s\S]*line-height:\s*1\.86;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(h3\)\s*\{[\s\S]*font-size:\s*21px;/)
|
2026-06-20 10:17:37 +08:00
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-grid\)\s*\{[\s\S]*border-left:\s*3px solid/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-focus-card\)\s*\{[\s\S]*background:\s*transparent;/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-step-index\)\s*\{[\s\S]*background:\s*transparent;[\s\S]*font-size:\s*17px;/)
|
2026-06-18 22:12:24 +08:00
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-date-popover\s*\{[\s\S]*animation:\s*workbenchAiPopoverIn/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-send-btn:not\(:disabled\)\s*\{[\s\S]*linear-gradient\(135deg,[\s\S]*#1d4ed8/)
|
|
|
|
|
assert.match(aiModeStyles, /\.workbench-ai-composer--inline\s*\{[\s\S]*min-height:\s*126px;[\s\S]*box-shadow:\s*none;/)
|
|
|
|
|
assert.match(aiModeStyles, /@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*\.workbench-ai-action,[\s\S]*\.workbench-ai-message,[\s\S]*\.workbench-ai-composer--inline,[\s\S]*\.workbench-ai-date-popover,[\s\S]*\.workbench-ai-thinking-dot\s*\{[\s\S]*animation:\s*none;/)
|
|
|
|
|
assert.ok(statSync(orbIconAsset).size > 100 * 1024)
|
|
|
|
|
assert.ok(statSync(orbIconAsset).size < 3 * 1024 * 1024)
|
|
|
|
|
assert.ok(statSync(orbIconPngAsset).size > 100 * 1024)
|
|
|
|
|
assert.equal(orbIconBuffer.subarray(0, 6).toString('ascii'), 'GIF89a')
|
|
|
|
|
assert.ok(countGifFrameBlocks(orbIconBuffer) >= 120)
|
|
|
|
|
const gifMotion = measureGifMotion(orbIconAsset)
|
|
|
|
|
assert.ok(gifMotion.seamDelta > gifMotion.medianAdjacentDelta * 0.35)
|
|
|
|
|
assert.ok(gifMotion.seamDelta < gifMotion.medianAdjacentDelta * 1.8)
|
|
|
|
|
assert.ok(measureGifDuration(orbIconAsset) >= 8000)
|
|
|
|
|
assert.ok(measureGifDuration(orbIconAsset) / countGifFrameBlocks(orbIconBuffer) <= 75)
|
|
|
|
|
const gifPresentation = measureOrbAssetPresentation(orbIconAsset)
|
|
|
|
|
assert.equal(gifPresentation.width, 192)
|
|
|
|
|
assert.equal(gifPresentation.height, 192)
|
|
|
|
|
assert.ok(gifPresentation.minimumCornerLuma > 225)
|
|
|
|
|
assert.ok(gifPresentation.maximumCornerLuma < 250)
|
|
|
|
|
assert.ok(gifPresentation.minimumBackgroundSimilarityRatio > 0.25)
|
|
|
|
|
assert.ok(gifPresentation.minimumForegroundWidthRatio > 0.9)
|
|
|
|
|
assert.ok(gifPresentation.minimumForegroundHeightRatio > 0.9)
|
|
|
|
|
const pngPresentation = measureOrbAssetPresentation(orbIconPngAsset)
|
|
|
|
|
assert.ok(pngPresentation.minimumCornerLuma > 225)
|
|
|
|
|
assert.ok(pngPresentation.maximumCornerLuma < 250)
|
|
|
|
|
assert.ok(pngPresentation.minimumBackgroundSimilarityRatio > 0.25)
|
|
|
|
|
assert.ok(pngPresentation.minimumForegroundWidthRatio > 0.9)
|
|
|
|
|
assert.ok(pngPresentation.minimumForegroundHeightRatio > 0.9)
|
|
|
|
|
})
|