Merge remote-tracking branch 'upstream/main'

# Conflicts:
#	openless-all/app/src/pages/QaPanel.tsx
This commit is contained in:
millionart committed 2026-05-04 20:08:37 +08:00
commit 8aefb4fea7
17 files changed
+172 -24

No files matched your search

+27
View File
@@ -187,6 +187,33 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: bash scripts/build-mac.sh
# ── Windows:先 build OpenLessIme.dll(x64+x86),再跑 tauri bundle。
# openless-ime.wxs 用 $(env.OPENLESS_IME_DLL_X64) / _X86 拿绝对路径,
# 跨 candle/light cwd 都能 resolve(Tauri wix bundler cwd 不固定)。
- name: Build Windows IME native DLLs
if: matrix.platform == 'windows-latest'
shell: pwsh
working-directory: 'openless-all/app'
run: |
$appRoot = (Resolve-Path .).Path
foreach ($t in @(
@{ Platform = 'x64'; Folder = 'x64'; EnvName = 'OPENLESS_IME_DLL_X64' },
@{ Platform = 'Win32'; Folder = 'x86'; EnvName = 'OPENLESS_IME_DLL_X86' }
)) {
$out = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\$($t.Folder)\Release"
$obj = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\obj\$($t.Folder)\Release"
./scripts/windows-ime-build.ps1 -Configuration Release -Platform $t.Platform -OutputDirectory $out -IntermediateDirectory $obj
if ($LASTEXITCODE -ne 0) {
throw "OpenLessIme $($t.Platform) build failed with exit $LASTEXITCODE"
}
$dll = (Resolve-Path (Join-Path $out 'OpenLessIme.dll')).Path
if (-not (Test-Path $dll)) {
throw "OpenLessIme.dll not produced at $dll"
}
"$($t.EnvName)=$dll" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "[ok] built $dll (exported $($t.EnvName))"
}
# ── Windows:仅 Windows 跑通用 tauri build(Linux 用下方专用 step,避免重复构建)──
- name: Build (Windows)
if: matrix.platform == 'windows-latest'
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openless-app",
"version": "1.2.13",
"version": "1.2.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openless-app",
"version": "1.2.13",
"version": "1.2.14",
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-autostart": "^2.5.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openless-app",
"private": true,
"version": "1.2.13",
"version": "1.2.14",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3281,7 +3281,7 @@ dependencies = [
[[package]]
name = "openless"
version = "1.2.13"
version = "1.2.14"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "openless"
version = "1.2.13"
version = "1.2.14"
description = "OpenLess — local voice input that types where your cursor is"
authors = ["OpenLess"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenLess",
"version": "1.2.13",
"version": "1.2.14",
"identifier": "com.openless.app",
"build": {
"beforeDevCommand": "npm run dev",
@@ -5,12 +5,12 @@
<Directory Id="OpenLessImeDirectory" Name="windows-ime">
<Directory Id="OpenLessImeX64Directory" Name="x64">
<Component Id="OpenLessImeDllX64Component" Guid="{B787DE6F-DF71-4D68-9DE8-04D3665A26F3}" Win64="yes">
<File Id="OpenLessImeDllX64" Name="OpenLessIme.dll" Source="src-tauri\target\windows-ime-msvc\x64\Release\OpenLessIme.dll" KeyPath="yes" />
<File Id="OpenLessImeDllX64" Name="OpenLessIme.dll" Source="$(env.OPENLESS_IME_DLL_X64)" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="OpenLessImeX86Directory" Name="x86">
<Component Id="OpenLessImeDllX86Component" Guid="{A2E61B9A-4034-4ABF-A42F-964A9B1F08BB}" Win64="no">
<File Id="OpenLessImeDllX86" Name="OpenLessIme.dll" Source="src-tauri\target\windows-ime-msvc\x86\Release\OpenLessIme.dll" KeyPath="yes" />
<File Id="OpenLessImeDllX86" Name="OpenLessIme.dll" Source="$(env.OPENLESS_IME_DLL_X86)" KeyPath="yes" />
</Component>
</Directory>
</Directory>
@@ -260,6 +260,14 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
两列内部各自的 overflow:auto 才能独立滚动 */}
<div
key={displayTab}
// issue #243:所有 tab 都允许 overflow:auto,让窗口被压缩 / 文案
// 变长时仍可触达底部内容(Codex P1:之前 overview 用 hidden
// 会让缩窗后 Recent 卡彻底不可见)。
// - Overview 借 Overview.tsx 内部 flex 把底部行 grow 到撑满,
// 正常尺寸下内容刚好占满 → 浏览器自动不显示 scrollbar;
// 真挤不下了才 fallback 出细滚动条。
// - 其他 tab 同样走细滚动条。
className="ol-thinscroll"
style={{
flex: 1, minHeight: 0,
overflow: 'auto',
@@ -164,7 +164,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
</div>
) : (
// personalize / about 短内容:单一 scroll wrapper,超出时本块滚动。
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '10px 28px 28px' }}>
<div className="ol-thinscroll" style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '10px 28px 28px' }}>
{section === 'personalize' && <PersonalizeSection />}
{section === 'about' && <AboutMini />}
</div>
@@ -0,0 +1,34 @@
import { renderQaMarkdown } from './qaMarkdown';
function assertIncludes(text: string, expected: string, name: string) {
if (!text.includes(expected)) {
throw new Error(`${name}: expected to include "${expected}", got "${text}"`);
}
}
function assertNotIncludes(text: string, expected: string, name: string) {
if (text.includes(expected)) {
throw new Error(`${name}: expected not to include "${expected}", got "${text}"`);
}
}
const htmlEscaped = renderQaMarkdown('<img src=x onerror=alert(1)><script>alert(1)</script>');
assertIncludes(htmlEscaped, '&lt;img src=x onerror=alert(1)&gt;', 'raw html should be escaped');
assertNotIncludes(htmlEscaped, '<script>', 'script tag should not be rendered');
assertNotIncludes(htmlEscaped, '<img src=x onerror=alert(1)>', 'raw html img should not become live dom tag');
const badHref = renderQaMarkdown('[xss](javascript:alert(1))');
assertNotIncludes(badHref, 'href="javascript:alert(1)"', 'javascript href should be dropped');
const goodMarkdown = renderQaMarkdown('**bold**\n\n- a\n- b\n\n`code`\n\n[ok](https://example.com)');
assertIncludes(goodMarkdown, '<strong>bold</strong>', 'bold markdown should render');
assertIncludes(goodMarkdown, '<li>a</li>', 'list markdown should render');
assertIncludes(goodMarkdown, '<code>code</code>', 'code markdown should render');
assertIncludes(goodMarkdown, 'href="https://example.com"', 'safe link should render');
const safeQueryLink = renderQaMarkdown('[ok](https://example.com?a=1&b=2)');
assertIncludes(safeQueryLink, 'href="https://example.com?a=1&amp;b=2"', 'safe query link should keep a single escaped ampersand');
const codeSnippet = renderQaMarkdown('`<div class=\"x\">`');
assertIncludes(codeSnippet, '&lt;div class=&quot;x&quot;&gt;', 'inline code should stay single-escaped');
assertNotIncludes(codeSnippet, '&amp;lt;', 'inline code should not be double-escaped');
+65
View File
@@ -0,0 +1,65 @@
import { marked } from 'marked';
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
function escapeHtml(raw: string): string {
return raw
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function decodeHtmlEntities(raw: string): string {
return raw
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
function normalizeHref(href: string | null | undefined): string | null {
if (!href) return null;
const trimmed = href.trim();
if (!trimmed) return null;
if (trimmed.startsWith('#')) return trimmed;
try {
const url = new URL(trimmed, 'https://openless.local/');
if (!SAFE_LINK_PROTOCOLS.has(url.protocol)) return null;
return trimmed;
} catch {
return null;
}
}
const QA_MARKDOWN_RENDERER = new marked.Renderer();
QA_MARKDOWN_RENDERER.html = (html: string) => escapeHtml(html);
QA_MARKDOWN_RENDERER.link = (href: string | null, title: string | null, text: string) => {
const safeHref = normalizeHref(decodeHtmlEntities(href ?? ''));
if (!safeHref) return `<span>${text}</span>`;
const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
return `<a href="${escapeHtml(safeHref)}"${titleAttr} target="_blank" rel="noreferrer noopener">${text}</a>`;
};
QA_MARKDOWN_RENDERER.image = (href: string | null, title: string | null, text: string) => {
const safeHref = normalizeHref(decodeHtmlEntities(href ?? ''));
if (!safeHref) return '';
const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
const alt = escapeHtml(text || '');
return `<img src="${escapeHtml(safeHref)}" alt="${alt}"${titleAttr} loading="lazy" />`;
};
export function renderQaPlainText(raw: string): string {
return `<pre>${escapeHtml(raw)}</pre>`;
}
export function renderQaMarkdown(markdown: string): string {
// 保留 markdown 语义(尤其代码块),但把 raw HTML token 转义为纯文本,避免注入。
return marked.parse(markdown, {
async: false,
gfm: true,
breaks: true,
renderer: QA_MARKDOWN_RENDERER,
}) as string;
}
+2 -2
View File
@@ -126,7 +126,7 @@ export function History() {
))}
</div>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: 6 }}>
<div className="ol-thinscroll" style={{ flex: 1, overflow: 'auto', padding: 6 }}>
{loading && <div style={{ padding: 16, fontSize: 12, color: 'var(--ol-ink-4)' }}>{t('common.loading')}</div>}
{!loading && filtered.length === 0 && (
<div style={{ padding: 16, fontSize: 12, color: 'var(--ol-ink-4)' }}>
@@ -164,7 +164,7 @@ export function History() {
</div>
</Card>
<Card padding={20} style={{ overflow: 'auto' }}>
<Card padding={20} className="ol-thinscroll" style={{ overflow: 'auto' }}>
{item ? (
<>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
+8 -5
View File
@@ -94,8 +94,11 @@ export function Overview({ onOpenHistory }: OverviewProps) {
<Metric icon="bolt" label={t('overview.metricTotal')} value={String(history.length)} trend={t('overview.metricTotalTrend')} accent />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 12 }}>
<Card padding={18}>
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。
只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。
issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 12, flex: 1, minHeight: 0 }}>
<Card padding={18} style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.weekTitle')}</span>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)' }}>{t('overview.weekUnit')}</span>
@@ -106,12 +109,12 @@ export function Overview({ onOpenHistory }: OverviewProps) {
</div>
</Card>
<Card padding={0}>
<div style={{ padding: '14px 18px', borderBottom: '0.5px solid var(--ol-line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Card padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
<div style={{ padding: '14px 18px', borderBottom: '0.5px solid var(--ol-line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.recentTitle')}</span>
<Btn size="sm" variant="ghost" onClick={onOpenHistory}>{t('overview.recentAll')}</Btn>
</div>
<div>
<div className="ol-thinscroll" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
{history.length === 0 && (
<div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)' }}>
{t('overview.recentEmpty', { trigger: getHotkeyBindingLabel(hotkey) })}
+5 -7
View File
@@ -12,15 +12,13 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { marked } from 'marked';
import { getSettings, isTauri, qaWindowDismiss, qaWindowPin } from '../lib/ipc';
import type { QaChatMessage, QaStatePayload, UserPreferences } from '../lib/types';
import { getHotkeyBindingLabel } from '../lib/hotkey';
import { renderQaMarkdown, renderQaPlainText } from '../lib/qaMarkdown';
const SELECTION_PREVIEW_MAX = 60;
marked.setOptions({ gfm: true, breaks: true });
type Status = 'idle' | 'recording' | 'thinking' | 'error';
export function QaPanel() {
@@ -405,10 +403,10 @@ function MessageRow({ message }: { message: QaChatMessage }) {
const html = useMemo(() => {
if (message.role !== 'assistant') return '';
try {
return marked.parse(message.content, { async: false }) as string;
return renderQaMarkdown(message.content);
} catch (error) {
console.error('[qa] failed to render markdown', error);
return message.content;
return renderQaPlainText(String(message.content ?? ''));
}
}, [message.content, message.role]);
@@ -445,10 +443,10 @@ function MessageRow({ message }: { message: QaChatMessage }) {
function StreamingAssistantBubble({ markdown }: { markdown: string }) {
const html = useMemo(() => {
try {
return marked.parse(markdown, { async: false }) as string;
return renderQaMarkdown(markdown);
} catch (error) {
console.error('[qa] failed to render streaming markdown', error);
return markdown;
return renderQaPlainText(String(markdown ?? ''));
}
}, [markdown]);
return (
+1
View File
@@ -117,6 +117,7 @@ export function Settings({ embedded = false, initialSection = 'recording' }: Set
))}
</div>
<div
className={embedded ? 'ol-thinscroll' : undefined}
style={{
display: 'flex',
flexDirection: 'column',
+3 -1
View File
@@ -32,11 +32,13 @@ interface CardProps {
style?: CSSProperties;
padding?: number;
glassy?: boolean;
className?: string;
}
export function Card({ children, style, padding = 18, glassy = false }: CardProps) {
export function Card({ children, style, padding = 18, glassy = false, className }: CardProps) {
return (
<div
className={className}
style={{
background: glassy ? 'rgba(255,255,255,0.55)' : 'var(--ol-surface)',
backdropFilter: glassy ? 'blur(20px) saturate(160%)' : undefined,
+10
View File
@@ -78,6 +78,16 @@ body {
.ol-noscrollbar::-webkit-scrollbar { display: none; }
.ol-noscrollbar { scrollbar-width: none; }
/* Thin scrollbar — 细而可见,保留"可以滚"的可见性,但不抢眼。issue #243 */
.ol-thinscroll::-webkit-scrollbar { width: 6px; height: 6px; }
.ol-thinscroll::-webkit-scrollbar-track { background: transparent; }
.ol-thinscroll::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.18);
border-radius: 3px;
}
.ol-thinscroll::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.28); }
.ol-thinscroll { scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.18) transparent; }
/* Glass surface */
.ol-glass {
background: var(--ol-glass-bg);