diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeCaps.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeCaps.ets index 419074a..f3d0a22 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeCaps.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeCaps.ets @@ -426,98 +426,3 @@ export function capDeviceInfo(deviceId: string, deviceName: string): CapResult { }; return okResult(JSON.stringify(payload)); } - -// ===== screensue 内容解析 ===== -// 服务端协议: screensue [秒] <内容>;0=常驻。 - -export interface ScreensuePayload { - duration: number; // 秒;0 表示常驻直到用户关闭 - content: string; -} - -export function parseScreensue(rawArgs: string): ScreensuePayload { - const p: ScreensuePayload = { duration: 5, content: '' }; - const leadingSpaces: RegExp = new RegExp('^\\s+'); - const firstSpace: RegExp = new RegExp('\\s'); - let rest: string = rawArgs.replace(leadingSpaces, ''); - const splitAt: number = rest.search(firstSpace); - if (splitAt > 0) { - const first: string = rest.substring(0, splitAt); - const digits: RegExp = new RegExp('^\\d+$'); - if (digits.test(first)) { - p.duration = Math.min(parseInt(first, 10), 86400); - rest = rest.substring(splitAt).replace(leadingSpaces, ''); - } - } - p.content = rest; - return p; -} - -/** - * 判断 agent 下发的 screensue 内容是不是 HTML 片段。 - * - * 服务端协议两侧都允许 HTML(localuse 的 local_screensue 在 Linux 用 browsh/w3m - * 渲染 HTML;remotedevice 的工具说明写的就是「显示内容/HTML」)。此前鸿蒙端一律 - * 塞进 Text(),于是 HTML 被当成普通字符串原样显示成标签。 - * - * 判据取「首个非空字符是 '<'」并且「存在配对的 '>'」且含字母/斜杠, - * 避免把 "<3" 这类纯文本误判成标签;再排除纯文本里常见的比较式(如 "a < b")。 - */ -export function looksLikeHtml(content: string): boolean { - const trimmed: string = content.trim(); - if (trimmed.length === 0 || trimmed.charAt(0) !== '<') { - return false; - } - const close: number = trimmed.indexOf('>'); - if (close < 0) { - return false; - } - // 标签名必须是字母开头(、

),"< 3" 这类不是标签。 - const inner: string = trimmed.substring(1, close).trim(); - if (inner.length === 0) { - return false; - } - const head: string = inner.charAt(0) === '/' ? inner.substring(1) : inner; - const tagName: RegExp = new RegExp('^[a-zA-Z][a-zA-Z0-9]*'); - return tagName.test(head); -} - -/** - * 把一段内容包成可交给 RichText 渲染的最小 HTML 文档。 - * - * 为什么用 RichText 而不是 Web:screensue 的内容是 agent 下发的第三方文本, - * Web 组件默认带 JS 与网络能力(javaScriptAccess/fileAccess),等于让远端内容 - * 在客户端进程里执行脚本。RichText 只解析 HTML 子集、无脚本、无网络, - * 对「给用户看一段内容」这个用途正好够用且更安全。 - * - * RichText 用系统默认前景色,深色主题下会变成黑字看不见 —— 这里显式注入 - * 颜色/字号,让两种主题下都可读。 - */ -export function screensueHtml(content: string, dark: boolean): string { - const fg: string = dark ? '#E8ECF4' : '#1B2430'; - const trimmed: string = content.trim(); - // 已是完整文档就不要重复包 (RichText 需要单一根节点才稳定)。 - const body: string = looksLikeHtml(trimmed) ? trimmed : escapeHtmlText(trimmed); - return '
' + body + '
'; -} - -/** HTML 转义(纯文本走 RichText 时用,避免内容里的 < > 被当成标签)。 */ -export function escapeHtmlText(s: string): string { - let out: string = ''; - for (let i = 0; i < s.length; i++) { - const c: string = s.charAt(i); - if (c === '&') { - out += '&'; - } else if (c === '<') { - out += '<'; - } else if (c === '>') { - out += '>'; - } else if (c === '"') { - out += '"'; - } else { - out += c; - } - } - return out; -} diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeRouter.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeRouter.ets index a6eabe1..1aa4d12 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeRouter.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/BridgeRouter.ets @@ -9,9 +9,8 @@ import { capSpeakerUse, capDeviceInfo, capStatus, - parseScreensue, - ScreensuePayload, } from './BridgeCaps'; +import { parseScreensue, ScreensuePayload } from './ScreensueHtml'; import { connStore } from './ConnStore'; import { common } from '@kit.AbilityKit'; diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/ScreensueHtml.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/ScreensueHtml.ets new file mode 100644 index 0000000..132f7ad --- /dev/null +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/ScreensueHtml.ets @@ -0,0 +1,182 @@ +/** + * screensue 载荷解析与 HTML 渲染(无 UI 依赖)。 + * + * 从 BridgeCaps.ets 抽出:加进 HTML 检测/编码后那个文件超过 520 行, + * 超出工程「单文件 ≤400 行」的约定;而「screensue 内容怎么解析、怎么渲染」 + * 与「设备能力怎么实现」本就是两件事。 + */ + +import { util } from '@kit.ArkTS'; + +// ===== screensue 内容解析 ===== +// 服务端协议: screensue [秒] <内容>;0=常驻。 + +export interface ScreensuePayload { + duration: number; // 秒;0 表示常驻直到用户关闭 + content: string; +} + +export function parseScreensue(rawArgs: string): ScreensuePayload { + const p: ScreensuePayload = { duration: 5, content: '' }; + const leadingSpaces: RegExp = new RegExp('^\\s+'); + const firstSpace: RegExp = new RegExp('\\s'); + let rest: string = rawArgs.replace(leadingSpaces, ''); + const splitAt: number = rest.search(firstSpace); + if (splitAt > 0) { + const first: string = rest.substring(0, splitAt); + const digits: RegExp = new RegExp('^\\d+$'); + if (digits.test(first)) { + p.duration = Math.min(parseInt(first, 10), 86400); + rest = rest.substring(splitAt).replace(leadingSpaces, ''); + } + } + p.content = rest; + return p; +} + +/** + * 判断 agent 下发的 screensue 内容是不是 HTML。 + * + * 服务端两侧协议都允许 HTML(localuse 的 local_screensue 在 Linux 用 browsh/w3m + * 渲染 HTML;remotedevice 的工具说明写的就是「显示内容/HTML」)。 + * + * ★ 判据必须容忍前导杂质:实测 agent 常把整段文档连引号一起传进来 + * (`'…'`),而"首个非空字符必须是 '<'"的旧判据直接判否、 + * 退回纯文本渲染,用户看到的就是满屏标签源码(截图取证)。 + * + * 所以这里扫到第一个「像标签开头」的 '<',不要求它在开头;但只有后面紧根 + * 字母或 '/' 时才认,避免把 "a < b" 这类文本里的比较符当标签。 + */ +export function looksLikeHtml(content: string): boolean { + return findHtmlStart(content) >= 0; +} + +/** 找到第一个「像标签开头」的 '<';没有则 -1。 */ +function findHtmlStart(content: string): number { + for (let i = 0; i < content.length; i++) { + if (content.charAt(i) !== '<') { + continue; + } + const next: string = i + 1 < content.length ? content.charAt(i + 1) : ''; + if (next === '/') { + const after: string = i + 2 < content.length ? content.charAt(i + 2) : ''; + if (isAsciiLetter(after)) { + return i; + } + continue; + } + if (isAsciiLetter(next)) { + return i; + } + } + return -1; +} + +function isAsciiLetter(ch: string): boolean { + if (ch.length === 0) { + return false; + } + const c: number = ch.charCodeAt(0); + return (c >= 65 && c <= 90) || (c >= 97 && c <= 122); +} + +/** + * 取出真正的 HTML 片段:剥掉 agent 误带的包裹引号,再从头截到第一个标签。 + * + * 剥引号是必须的:不剥的话那个孤立的 `'` 会被 Web 当正文渲染出来 + * (截图上第一行就是它),而且它还会把后续判据带偏。返回 '' 表示不是 HTML。 + */ +export function screensueHtmlDocument(content: string): string { + let body: string = content.trim(); + // 反复剥成对的包裹引号(agent 把整段 HTML 当命令参数传时的常见形态)。 + while (body.length >= 2) { + const first: string = body.charAt(0); + const last: string = body.charAt(body.length - 1); + if ((first === '\'' && last === '\'') || (first === '"' && last === '"')) { + body = body.substring(1, body.length - 1).trim(); + continue; + } + break; + } + const idx: number = findHtmlStart(body); + if (idx < 0) { + return ''; + } + if (idx > 0) { + body = body.substring(idx); + } + return body; +} + +/** + * 把 screensue 内容编成可直接交给 Web 组件 `loadData` 的 base64。 + * + * 为什么必须上 Web(不再用 RichText):RichText 只认极小标签子集, + * 对 ' + + fragment + ''; + return base64Utf8(wrapped); +} + +/** 内容是否已是完整 HTML 文档(有 或 ),不必再包壳。 */ +function hasHtmlShell(s: string): boolean { + const head: string = s.substring(0, 400).toLowerCase(); + return head.indexOf('= 0 || head.indexOf('= 0; +} + +/** + * UTF-8 字符串 → base64。 + * + * 不能把 UTF-16 码元直接交给 Base64Helper:那样中文会变成乱码。 + * 这里手写 UTF-8 字节序列(按码点,含代理对合成)后再编码。 + */ +export function base64Utf8(s: string): string { + const bytes: number[] = []; + for (let i = 0; i < s.length; i++) { + let code: number = s.charCodeAt(i); + // 代理对(emoji 等)合成成一个码点。 + if (code >= 0xD800 && code <= 0xDBFF && i + 1 < s.length) { + const next: number = s.charCodeAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + code = ((code - 0xD800) << 10) + (next - 0xDC00) + 0x10000; + i++; + } + } + if (code < 0x80) { + bytes.push(code); + } else if (code < 0x800) { + bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F)); + } else { + bytes.push(0xF0 | (code >> 18), 0x80 | ((code >> 12) & 0x3F), + 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F)); + } + } + const helper: util.Base64Helper = new util.Base64Helper(); + return helper.encodeToStringSync(new Uint8Array(bytes)); +} diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ScreensuePage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ScreensuePage.ets index 45f9d9b..893fe6a 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/ScreensuePage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/ScreensuePage.ets @@ -2,7 +2,8 @@ import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, ANIM_NORMAL } from '../commo import { GradientBackground } from './GradientBackground'; import { PageTopBar } from './PageTopBar'; import { MotionBase } from './MotionBase'; -import { looksLikeHtml, screensueHtml } from '../common/BridgeCaps'; +import { looksLikeHtml, screensueWebData } from '../common/ScreensueHtml'; +import { webview } from '@kit.ArkWeb'; /** * agent 主动推送的前台内容页。 @@ -10,9 +11,15 @@ import { looksLikeHtml, screensueHtml } from '../common/BridgeCaps'; * 调用方负责决定页面宽度:窄屏占满窗口,宽屏只占右侧内容栏, * 从而让左侧一级页面和主导航保持可见、可操作。 * - * 内容可能是纯文本,也可能是 HTML(服务端两侧协议都允许,见 BridgeCaps 的 - * screensueHtml)。HTML 用 RichText 渲染:它只解析 HTML 子集、无脚本、无网络, - * 对 agent 下发的远端内容比 Web 组件安全(Web 默认带 JS 与网络能力)。 + * 内容可能是纯文本,也可能是 HTML(服务端两侧协议都允许,见 ScreensueHtml)。 + * + * HTML 走 **Web 组件**(用户明确要求):agent 推的常是完整文档 —— 带