feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#787
feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#787bubbmon233 wants to merge 1 commit intolarksuite:mainfrom
Conversation
Add an HTML lint library + Larksuite-native autofix to lark-cli mail, plus the skills/lark-mail/ skill bundle (2 reference docs, 5 HTML templates, the +lint-html shortcut, and writing-path lint integration across all 6 compose shortcuts). Lint library (shortcuts/mail/lint/) - Error: drop dangerous tags (<script> / <iframe> / <form> / <input> / <link> / <object> / <embed>), on* event handlers, javascript: / vbscript: / file: URLs. - Warning + autofix: rewrite HTML4-era <font> / <center> / <marquee> / <blink>. - Larksuite-native autofix: rewrite <p> / <ul> / <ol> / <li> / <blockquote> / <a> to mail-editor native markup so AI can write the simplest HTML and still produce native-quality rendering. - Inline-style and URL-scheme allow-list filtering. - <style> block passthrough (server adds CSS scope class). +lint-html shortcut (preview / CI) Read-only HTML preview tool. Default envelope returns only cleaned_html; --show-lint-details adds full warnings[] / errors[]. --strict exits non- zero on any finding (CI gate). Writing-path lint in 6 compose shortcuts +send / +draft-create / +reply / +reply-all / +forward / +draft-edit body op all run lint before drafting: - lint_applied_count / original_blocked_count: always present. - lint_applied[] / original_blocked[]: only with --show-lint-details. - compose_hint: points AI consumers to the HTML writing guide. skills/lark-mail/ skill bundle 5 pre-rendered Larksuite-native HTML templates: weekly newsletter, personal weekly report, team weekly report, market research report, résumé. 2 reference docs: - references/lark-mail-html.md: writing rules + format primitives + template-usage flow. - references/lark-mail-lint-html.md: +lint-html usage + return-value contract + 9 worked examples. SKILL.md updates linking the new docs and templates. Sealed conventions - @user mention chip: id="at-user-N" is the only hard requirement; do not write data-user-id. - Highlight palette: 3 colors (pink milestones, yellow follow-ups, green completed); black text, no bold / padding / border-radius. - Brand color palette: main black, 3 levels of grey, Lark blue / deep blue, alert red, emergency orange, light pink / light grey backgrounds, border grey. - URL scheme allow-list: http(s): / mailto: / cid: / data:image/* only. - Inline-style + tag allow-lists. - Writing-style floor: subject <= 50 chars, decision-first, lists instead of mechanical numbering, emoji only as status tags. Tests - shortcuts/mail/lint/...: unit tests for every rule. - shortcuts/mail/mail_lint_html_test.go: +lint-html envelope contract. - shortcuts/mail/mail_lint_writepath_test.go: writing-path envelope contract. - 5 templates verified via +draft-create smoke test.
📝 WalkthroughWalkthroughThis PR introduces a complete HTML linting and auto-fixing pipeline for mail body composition. It adds a core lint engine with DOM-based sanitization and rule classification, integrates write-path linting into all compose shortcuts with optional reporting, publishes a new ChangesLint Engine Core
Lint Testing and Integration
Compose Shortcut Integration
Documentation and Templates
Sequence Diagram(s)sequenceDiagram
participant User
participant MailDraft as +draft-create
participant BuildEML as buildRawEMLForDraftCreate
participant WriteLint as runWritePathLint
participant Lint as lint.Run
participant API as drafts.create
User->>MailDraft: --body="<font>Hi</font>"<br/>--show-lint-details
MailDraft->>BuildEML: HTML body
BuildEML->>WriteLint: HTML (AutoFix=true,<br/>Strict=false)
WriteLint->>Lint: Parse, walk, classify
Lint-->>WriteLint: Applied:[rewrite],<br/>Cleaned:"<span>Hi</span>"
WriteLint-->>BuildEML: cleaned HTML +<br/>lintApplied/lintBlocked
BuildEML->>BuildEML: Build EML with<br/>cleaned HTML
BuildEML-->>MailDraft: rawEML, lintApplied,<br/>lintBlocked, nil
MailDraft->>API: drafts.create(rawEML)
API-->>MailDraft: draft_id
MailDraft->>MailDraft: applyLintToEnvelope<br/>(show full arrays)
MailDraft->>MailDraft: addComposeHint
MailDraft-->>User: JSON envelope:<br/>draft_id, lint_applied[],<br/>lint_applied_count,<br/>compose_hint
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes The PR spans a major new feature (lint library + integration) with 1065 lines of linting logic, 901 lines of tests, ~200 lines of classification rules, new DOM manipulation logic, integration into 6+ compose shortcuts with modified signatures, write-path linting helpers, and comprehensive documentation. The lint engine involves intricate DOM traversal, multiple rule classification tiers, tag rewriting logic, and Feishu-native style augmentation. While individual files follow consistent patterns, the variety of behaviors (error deletion, warning rewriting, style sanitization, URL validation, Feishu augmentation) and cross-file integration demands careful review of logic density and edge case handling. Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
|
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@094b9992ce5cb08f980f6a4cabaa39b5b3d973a0🧩 Skill updatenpx skills add bubbmon233/cli#feat/lark-mail -y -g |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
shortcuts/mail/mail_lint_html_test.go (1)
157-183: ⚡ Quick winLGTM — tests cleanly cover the
--auto-fix=false/--show-lint-detailsenvelope contract.One gap to complement the issue raised in
mail_lint_html.go: consider adding a sub-case toTestMailLintHTML_AutoFixFalseOmitsCleanedHTMLthat omits--show-lint-detailsand asserts on the resulting envelope shape (empty data, or explicitly documented fields), so the contract for that combination is pinned.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shortcuts/mail/mail_lint_html_test.go` around lines 157 - 183, Add a sub-case to TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that calls runMountedMailShortcut with the same flags except without "--show-lint-details" (use MailLintHTML, runMountedMailShortcut and decodeShortcutEnvelopeData to exercise the shortcut), then decode the envelope and assert the expected shape for the --auto-fix=false + no --show-lint-details combination (e.g., that data is empty or only contains the explicitly documented fields); keep assertions deterministic and mirror existing test style so the contract is pinned alongside the existing subcase.shortcuts/mail/mail_lint_writepath_test.go (1)
15-17: 💤 Low valueDrop the double-helper indirection around
json.Unmarshal.
jsonDecoderUnmarshal(line 17) andjsonUnmarshal(line 350) are both one-line wrappers that ultimately just callencoding/json.Unmarshal. The stated rationale ("to keep the import set explicit") is already achieved by a single wrapper or by callingjson.Unmarshaldirectly at the one call site (line 340). The second indirection adds no value.♻️ Proposed simplification
-// jsonDecoderUnmarshal is a thin alias used by helpers in this file to keep -// the import set explicit even when the helper would otherwise be one-line. -func jsonDecoderUnmarshal(b []byte, v interface{}) error { return json.Unmarshal(b, v) } - @@ var captured map[string]interface{} - if err := jsonUnmarshal(stub.CapturedBody, &captured); err != nil { + if err := json.Unmarshal(stub.CapturedBody, &captured); err != nil { t.Fatalf("decode captured body: %v", err) } @@ -func jsonUnmarshal(b []byte, v interface{}) error { - return jsonDecoderUnmarshal(b, v) -}Also applies to: 350-352
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shortcuts/mail/mail_lint_writepath_test.go` around lines 15 - 17, Remove the unnecessary double-wrapper around encoding/json.Unmarshal: eliminate either jsonDecoderUnmarshal or jsonUnmarshal and update callers to use the remaining wrapper (or call json.Unmarshal directly). Specifically, remove the one-line function jsonDecoderUnmarshal and replace its uses with jsonUnmarshal (or remove jsonUnmarshal and replace its uses with json.Unmarshal) so there is only a single indirection; update the call site referenced around line ~340 to call the chosen function (jsonUnmarshal or json.Unmarshal) and remove the redundant helper definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@shortcuts/mail/lint/linter.go`:
- Around line 500-505: truncateExcerpt currently slices by bytes
(s[:MaxExcerptBytes-4]) which can cut a UTF‑8 codepoint and produce invalid UTF
sequences; change it to truncate on rune boundaries instead by iterating runes
(for range over s) or converting to []rune and accumulating byte lengths until
reaching MaxExcerptBytes-4, then return the collected valid-prefix string + "
..."; update the function truncateExcerpt and keep MaxExcerptBytes logic but
ensure you never split a multi-byte rune.
In `@shortcuts/mail/mail_lint_html.go`:
- Around line 52-53: The flag description for "--auto-fix" is inaccurate: it
claims violations are returned when false but the code only emits
warnings/errors if "--show-lint-details" is set (see where warnings/errors
arrays are populated), so update the description in the flag definitions (the
entries that include Name: "auto-fix" and Name: "strict") to state that when
"--auto-fix=false" the cleaned_html is omitted and violation arrays are only
included if "--show-lint-details" is also set; also add a sub-case to
TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that runs with "--auto-fix=false"
but without "--show-lint-details" and asserts the documented empty/omitted data
behavior.
In `@skill-template/domains/mail.md`:
- Around line 286-288: The two links in skill-template/domains/mail.md pointing
to references/lark-mail-html-allowlist.md and
references/lark-mail-feishu-native.md are broken because those files don't
exist; update the list to point to the existing references/lark-mail-html.md
(replace the allowlist link) and either add or remove the feishu-native
reference (replace with lark-mail-lint-html.md if that was intended or delete
the line), ensuring the link targets match the actual filenames
(references/lark-mail-html.md and references/lark-mail-lint-html.md) and that
link text reflects the replaced document.
- Around line 309-313: Update the mail skill doc to stop claiming `<style>` is
deleted: remove `<style>` from the "直接删除" list in skill-template/domains/mail.md
and add a one-line note that `<style>` blocks are passed through (see
shortcuts/mail/lint/types.go doc) and that CSS is scoped/handled server-side;
also mention classifyTag / blockedTags in shortcuts/mail/lint/rules.go for
accuracy so readers know `<style>` is allowed by the linter and templates like
skills/lark-mail/assets/templates/research--market-report.html rely on that
passthrough.
In `@skills/lark-mail/assets/templates/weekly--team-report.html`:
- Around line 8-11: The template contains invalid list nesting where child
<ul>/<ol> tags are direct children of other lists instead of being inside an
<li> (e.g., the repeated leading <ul data-list-bullet="true"> and the <ol ...
data-list-number="true"> that sit beside <li class="temp-li bullet2"> and <li
class="temp-li number1">); fix by moving each nested <ul> or <ol> so it is a
child of the appropriate preceding <li> (for example place the inner <ul
data-list-bullet="true"> that contains <li class="temp-li bullet2"> inside the
parent <li class="temp-li number1"> or the corresponding parent <li
class="temp-li bullet2">), remove any duplicated wrapper lists, and ensure every
<ul>/<ol> direct child is only <li> elements and any sublists are nested inside
those <li> elements (check class names like temp-li bullet2, temp-li bullet3,
and number1 to verify correct placement).
In `@skills/lark-mail/references/lark-mail-forward.md`:
- Around line 16-17: The line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken relative link; remove the erroneous
closing "**" and correct the markdown link so the link text and target point to
the actual file name in the same directory (e.g., use lark-mail-html.md or
./lark-mail-html.md) ensuring the link is valid and the bold markers are
balanced.
In `@skills/lark-mail/references/lark-mail-html.md`:
- Around line 11-18: Replace the two typos in the guide text: change the phrase
"处于安全考虑" to "出于安全考虑" (the first sentence about the built-in HTML lint tool) and
change "不正文长度" to "不限制正文长度" (the "正文长度自适应" bullet) so the wording reads
correctly and preserves the original guidance.
In `@skills/lark-mail/references/lark-mail-reply-all.md`:
- Around line 16-17: The line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken/duplicated relative link; remove the
extraneous "**" and ensure the Markdown link to references/lark-mail-html.md is
valid (single [text](references/lark-mail-html.md)) so the sentence reads
cleanly and the link resolves correctly in lark-mail-reply-all.md.
In `@skills/lark-mail/references/lark-mail-reply.md`:
- Around line 20-21: The markdown line starting with "编辑邮件内容前 MUST 先用 Read 工具读取
..." has malformed bold markup (missing the opening "**") and an incorrect
relative link; fix it by adding the opening "**" before the sentence and
changing the link target from "references/lark-mail-html.md" to
"lark-mail-html.md" so it points to the file in the same directory (update the
line content accordingly).
In `@skills/lark-mail/references/lark-mail-send.md`:
- Around line 15-16: The sentence fragment containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray/malformed bold marker and a broken relative link; remove or correctly
place the trailing "**" and update the link target to the correct relative
filename (e.g. "lark-mail-html.md" or "./lark-mail-html.md") so the Markdown
link resolves, ensuring the text reads cleanly and the reference to
lark-mail-html.md is valid.
---
Nitpick comments:
In `@shortcuts/mail/mail_lint_html_test.go`:
- Around line 157-183: Add a sub-case to
TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that calls runMountedMailShortcut
with the same flags except without "--show-lint-details" (use MailLintHTML,
runMountedMailShortcut and decodeShortcutEnvelopeData to exercise the shortcut),
then decode the envelope and assert the expected shape for the --auto-fix=false
+ no --show-lint-details combination (e.g., that data is empty or only contains
the explicitly documented fields); keep assertions deterministic and mirror
existing test style so the contract is pinned alongside the existing subcase.
In `@shortcuts/mail/mail_lint_writepath_test.go`:
- Around line 15-17: Remove the unnecessary double-wrapper around
encoding/json.Unmarshal: eliminate either jsonDecoderUnmarshal or jsonUnmarshal
and update callers to use the remaining wrapper (or call json.Unmarshal
directly). Specifically, remove the one-line function jsonDecoderUnmarshal and
replace its uses with jsonUnmarshal (or remove jsonUnmarshal and replace its
uses with json.Unmarshal) so there is only a single indirection; update the call
site referenced around line ~340 to call the chosen function (jsonUnmarshal or
json.Unmarshal) and remove the redundant helper definition.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0b060b15-542c-4a8e-ab0d-1631f90a42f6
📒 Files selected for processing (31)
shortcuts/mail/lint/linter.goshortcuts/mail/lint/linter_test.goshortcuts/mail/lint/rules.goshortcuts/mail/lint/types.goshortcuts/mail/mail_draft_create.goshortcuts/mail/mail_draft_create_test.goshortcuts/mail/mail_draft_edit.goshortcuts/mail/mail_forward.goshortcuts/mail/mail_lint_html.goshortcuts/mail/mail_lint_html_test.goshortcuts/mail/mail_lint_writepath.goshortcuts/mail/mail_lint_writepath_test.goshortcuts/mail/mail_reply.goshortcuts/mail/mail_reply_all.goshortcuts/mail/mail_send.goshortcuts/mail/shortcuts.goskill-template/domains/mail.mdskills/lark-mail/SKILL.mdskills/lark-mail/assets/templates/job-application--resume.htmlskills/lark-mail/assets/templates/newsletter--weekly-brief.htmlskills/lark-mail/assets/templates/research--market-report.htmlskills/lark-mail/assets/templates/weekly--personal-report.htmlskills/lark-mail/assets/templates/weekly--team-report.htmlskills/lark-mail/references/lark-mail-draft-create.mdskills/lark-mail/references/lark-mail-draft-edit.mdskills/lark-mail/references/lark-mail-forward.mdskills/lark-mail/references/lark-mail-html.mdskills/lark-mail/references/lark-mail-lint-html.mdskills/lark-mail/references/lark-mail-reply-all.mdskills/lark-mail/references/lark-mail-reply.mdskills/lark-mail/references/lark-mail-send.md
| func truncateExcerpt(s string) string { | ||
| if len(s) <= MaxExcerptBytes { | ||
| return s | ||
| } | ||
| return s[:MaxExcerptBytes-4] + " ..." | ||
| } |
There was a problem hiding this comment.
truncateExcerpt may split a UTF-8 codepoint at byte 196.
The byte slice at s[:MaxExcerptBytes-4] cuts at a fixed byte offset, which can land in the middle of a multi-byte UTF-8 sequence (common for Chinese subjects/bodies in this PR's templates). When the resulting string is JSON-encoded for the envelope, Go's encoder replaces the invalid trailing bytes with \ufffd, leaving a garbled-looking excerpt to AI/CI consumers.
Backing off to the previous rune boundary fixes it cheaply.
♻️ Suggested fix using rune-aware truncation
+import (
+ "unicode/utf8"
+)
func truncateExcerpt(s string) string {
if len(s) <= MaxExcerptBytes {
return s
}
- return s[:MaxExcerptBytes-4] + " ..."
+ cut := MaxExcerptBytes - 4
+ // Back off to a rune boundary so we don't split a multi-byte sequence.
+ for cut > 0 && !utf8.RuneStart(s[cut]) {
+ cut--
+ }
+ return s[:cut] + " ..."
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/mail/lint/linter.go` around lines 500 - 505, truncateExcerpt
currently slices by bytes (s[:MaxExcerptBytes-4]) which can cut a UTF‑8
codepoint and produce invalid UTF sequences; change it to truncate on rune
boundaries instead by iterating runes (for range over s) or converting to []rune
and accumulating byte lengths until reaching MaxExcerptBytes-4, then return the
collected valid-prefix string + " ..."; update the function truncateExcerpt and
keep MaxExcerptBytes logic but ensure you never split a multi-byte rune.
| {Name: "auto-fix", Type: "bool", Default: "true", Desc: "When true (default), the response includes cleaned_html (HTML rewritten with warnings auto-fixed and errors removed). When false, only the violation list is returned and cleaned_html is omitted."}, | ||
| {Name: "strict", Type: "bool", Default: "false", Desc: "When true, all warnings are promoted to errors and the command exits non-zero on any finding. Useful as a CI gate for static HTML templates. Default false."}, |
There was a problem hiding this comment.
--auto-fix=false description contradicts actual envelope behavior.
The flag description states "only the violation list is returned and cleaned_html is omitted", but warnings/errors arrays are only emitted when --show-lint-details is also set (Lines 130–133). With --auto-fix=false alone (no --show-lint-details), data is an empty map — no findings, no cleaned HTML. This misleads AI consumers that use the flag description to understand the response contract.
Choose one of:
- Fix the description to note that
--show-lint-detailsis needed to surface the violation list. - Automatically expose findings when
--auto-fix=false(since there's no cleaned HTML to return, the violation list is the only useful output).
📝 Proposed description fix (option A)
-{Name: "auto-fix", Type: "bool", Default: "true", Desc: "When true (default), the response includes cleaned_html (HTML rewritten with warnings auto-fixed and errors removed). When false, only the violation list is returned and cleaned_html is omitted."},
+{Name: "auto-fix", Type: "bool", Default: "true", Desc: "When true (default), the response includes cleaned_html (HTML rewritten with warnings auto-fixed and errors removed). When false, cleaned_html is omitted; add --show-lint-details to surface the warnings[]/errors[] arrays."},TestMailLintHTML_AutoFixFalseOmitsCleanedHTML always pairs --auto-fix=false with --show-lint-details, so this gap isn't caught by tests. Consider adding a sub-case that omits --show-lint-details and asserts an empty (or otherwise documented) data object.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {Name: "auto-fix", Type: "bool", Default: "true", Desc: "When true (default), the response includes cleaned_html (HTML rewritten with warnings auto-fixed and errors removed). When false, only the violation list is returned and cleaned_html is omitted."}, | |
| {Name: "strict", Type: "bool", Default: "false", Desc: "When true, all warnings are promoted to errors and the command exits non-zero on any finding. Useful as a CI gate for static HTML templates. Default false."}, | |
| {Name: "auto-fix", Type: "bool", Default: "true", Desc: "When true (default), the response includes cleaned_html (HTML rewritten with warnings auto-fixed and errors removed). When false, cleaned_html is omitted; add --show-lint-details to surface the warnings[]/errors[] arrays."}, | |
| {Name: "strict", Type: "bool", Default: "false", Desc: "When true, all warnings are promoted to errors and the command exits non-zero on any finding. Useful as a CI gate for static HTML templates. Default false."}, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shortcuts/mail/mail_lint_html.go` around lines 52 - 53, The flag description
for "--auto-fix" is inaccurate: it claims violations are returned when false but
the code only emits warnings/errors if "--show-lint-details" is set (see where
warnings/errors arrays are populated), so update the description in the flag
definitions (the entries that include Name: "auto-fix" and Name: "strict") to
state that when "--auto-fix=false" the cleaned_html is omitted and violation
arrays are only included if "--show-lint-details" is also set; also add a
sub-case to TestMailLintHTML_AutoFixFalseOmitsCleanedHTML that runs with
"--auto-fix=false" but without "--show-lint-details" and asserts the documented
empty/omitted data behavior.
| - [HTML 兼容白名单](references/lark-mail-html-allowlist.md) — 标签 / class / inline style 速查;表格 / 列表 / 字号 / 引用 / 链接 / 内嵌图片标准写法 | ||
| - [飞书原生写法(含风格指引与 3 套完整模板)](references/lark-mail-feishu-native.md) — 通知 / 周报 / 决策请求三套漂亮模板(含问候开场 + 骨架 + 落款),直接替换变量即可使用 | ||
| - [`+lint-html` 用法](references/lark-mail-lint-html.md) — 创建草稿前自检 / 修复 AI 输出 / CI 校验静态 HTML 模板 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the three referenced files exist under skill-template/domains/references/.
fd -uu --type f -e md . | rg -n 'lark-mail'
echo '---'
fd -uu --type d 'references' | head
echo '---'
# Look for any in-tree references that already use the singular name (lark-mail-html.md)
rg -n --no-heading 'lark-mail-html(-allowlist|-feishu-native)?\.md|lark-mail-feishu-native\.md|lark-mail-lint-html\.md'Repository: larksuite/cli
Length of output: 5066
Two of the three referenced files do not exist.
Lines 286–287 reference lark-mail-html-allowlist.md and lark-mail-feishu-native.md, but only the following files exist in skill-template/domains/references/:
lark-mail-html.md(exists)lark-mail-lint-html.md(exists)
Update the links to reference files that actually exist, or remove references to the non-existent docs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skill-template/domains/mail.md` around lines 286 - 288, The two links in
skill-template/domains/mail.md pointing to
references/lark-mail-html-allowlist.md and references/lark-mail-feishu-native.md
are broken because those files don't exist; update the list to point to the
existing references/lark-mail-html.md (replace the allowlist link) and either
add or remove the feishu-native reference (replace with lark-mail-lint-html.md
if that was intended or delete the line), ensuring the link targets match the
actual filenames (references/lark-mail-html.md and
references/lark-mail-lint-html.md) and that link text reflects the replaced
document.
| `+send` / `+draft-create` / `+reply` / `+reply-all` / `+forward` / `+draft-edit` body op 在调用 `emlbuilder` **之前**会强制对 HTML 正文做 lint: | ||
|
|
||
| - 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<style>` / `<link>` 等)会被**直接删除** | ||
| - 警告(`<font>` / `<center>` / `<marquee>`)会被**自动修复**为飞书原生写法 | ||
| - 不允许的 CSS property(`position` / `z-index` / `transform` 等)会从 inline `style` 里删除 |
There was a problem hiding this comment.
<style> is documented as deleted but the lint implementation passes it through.
Line 311 lists <style> among the tags that are "直接删除" (directly deleted), but:
shortcuts/mail/lint/types.gopackage doc states: "<style>is passed through verbatim (Feishu mail server-side sanitiser handles it on render)".shortcuts/mail/lint/rules.godoes not includestyleinblockedTags, soclassifyTag("style")falls through to the default "allow" branch.- The new
skills/lark-mail/assets/templates/research--market-report.htmltemplate ships with a<style>block of class rules and depends on this passthrough behavior.
Since this skill doc is consumed by the AI agent to plan write paths, the false claim will lead it to avoid <style> blocks and miss the <style>-passthrough capability that templates rely on. Drop <style> from the deleted list (and ideally add a one-line note that <style> is passed through and CSS-scoped server-side).
📝 Suggested fix
-- 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<style>` / `<link>` 等)会被**直接删除**
+- 错误(`<script>` / `on*` / `javascript:` URL / `<iframe>` / `<form>` / `<link>` 等)会被**直接删除**
+- `<style>` 块原样透传(服务端会自动加 CSS scope class,避免污染收件人邮箱样式)
- 警告(`<font>` / `<center>` / `<marquee>`)会被**自动修复**为飞书原生写法
- 不允许的 CSS property(`position` / `z-index` / `transform` 等)会从 inline `style` 里删除🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skill-template/domains/mail.md` around lines 309 - 313, Update the mail skill
doc to stop claiming `<style>` is deleted: remove `<style>` from the "直接删除" list
in skill-template/domains/mail.md and add a one-line note that `<style>` blocks
are passed through (see shortcuts/mail/lint/types.go doc) and that CSS is
scoped/handled server-side; also mention classifyTag / blockedTags in
shortcuts/mail/lint/rules.go for accuracy so readers know `<style>` is allowed
by the linter and templates like
skills/lark-mail/assets/templates/research--market-report.html rely on that
passthrough.
| <ul data-list-bullet="true" style="margin-top:0px;margin-bottom:0px;margin-left:0px;padding-left:0px;list-style-position:inside"><ul data-list-bullet="true" style="margin:0px 0px 0px 24px;padding-left:0px;list-style-position:inside"><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 1.1:动作 / 产出,宏观角度]</span></span><a id="at-user-3" href="mailto:[email-子1.1]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 c]</a><span style="font-family:inherit"><span style="color:rgb(31,35,41)">,已完成</span></span></li><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 1.2]</span></span><a id="at-user-4" href="mailto:[email-子1.2]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 d]</a><span style="font-family:inherit"><span style="color:rgb(31,35,41)">,已完成</span></span></li><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 1.3]</span></span><a id="at-user-5" href="mailto:[email-子1.3a]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 e]</a><a id="at-user-6" href="mailto:[email-子1.3b]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 f]</a><span style="font-family:inherit"><span style="color:rgb(31,35,41)">,已完成</span></span></li></ul></ul> | ||
| <ol start="2" data-list-number="true" style="margin-top:0px;margin-bottom:0px;margin-left:0px;padding-left:0px;list-style-position:inside"><li class="temp-li number1" data-li-line="true" data-list="number1" data-ol-id="weekly-this" data-start="2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:decimal;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><b><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[项目 / 事件 2 名称]</span></span></b><a id="at-user-7" href="mailto:[email-2]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 g]</a></li></ol> | ||
| <blockquote style="padding-left:0px;color:rgb(100,106,115);border-left:2px solid rgb(187,191,196);margin:0px"><div dir="auto" style="font-size:14px;padding-left:12px"><span style="font-family:inherit"><span style="color:rgb(100,106,115)">技术方案:<a class="not-doclink" href="https://[doc-url-2a]" style="cursor:pointer;text-decoration:none;color:rgb(20,86,240)">[文档名]</a> · 设计稿:<a class="not-doclink" href="https://[doc-url-2b]" style="cursor:pointer;text-decoration:none;color:rgb(20,86,240)">[设计稿名]</a></span></span></div></blockquote> | ||
| <ul data-list-bullet="true" style="margin-top:0px;margin-bottom:0px;margin-left:0px;padding-left:0px;list-style-position:inside"><ul data-list-bullet="true" style="margin:0px 0px 0px 24px;padding-left:0px;list-style-position:inside"><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><b><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 2.1:含孙子项的动作主题]</span></span></b></li><ul data-list-bullet="true" style="margin:0px 0px 0px 24px;padding-left:0px;list-style-position:inside"><li class="temp-li bullet3" data-li-line="true" data-list="bullet3" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:square;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[孙子项 2.1.1:必要时再细分一层;不需要可整段删除]</span></span><a id="at-user-8" href="mailto:[email-孙2.1.1]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 h]</a></li><li class="temp-li bullet3" data-li-line="true" data-list="bullet3" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:square;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[孙子项 2.1.2]</span></span></li></ul><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 2.2]</span></span><a id="at-user-9" href="mailto:[email-子2.2]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 i]</a><span style="font-family:inherit"><span style="color:rgb(31,35,41)">,进行中</span></span></li><li class="temp-li bullet2" data-li-line="true" data-list="bullet2" style="line-height:1.6;margin-top:4px;margin-bottom:4px;padding-left:0px;display:list-item;list-style-type:circle;font-family:inherit;font-size:14px;margin-left:0px;list-style-position:inside" dir="auto"><span style="font-family:inherit"><span style="color:rgb(31,35,41)">[子项 2.3]</span></span><a id="at-user-10" href="mailto:[email-子2.3]" style="cursor:pointer;color:rgb(20,86,240);padding:2px;text-decoration:none;border-radius:999em;margin:0px 2px" rel="nofollow noopener noreferrer">@[姓名 j]</a><span style="font-family:inherit"><span style="color:rgb(31,35,41)">,评审中</span></span></li></ul></ul> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n skills/lark-mail/assets/templates/weekly--team-report.html | head -30Repository: larksuite/cli
Length of output: 16744
🏁 Script executed:
wc -l skills/lark-mail/assets/templates/weekly--team-report.html && echo "---" && cat skills/lark-mail/assets/templates/weekly--team-report.htmlRepository: larksuite/cli
Length of output: 16677
Fix invalid nested list structure to avoid renderer/sanitizer reflow
On lines 8, 11, 14, and 17, nested lists (<ul> or <ol>) are attached directly under parent list elements instead of being nested inside a parent <li>. This violates HTML5 content model rules (only <li> elements are allowed as direct children of <ul>/<ol>), and may render unpredictably after lint or autofix across mail clients.
Suggested structural fix pattern
-<ul ...>
- <ul ...>
- <li>...</li>
- </ul>
-</ul>
+<ul ...>
+ <li ...>
+ ...
+ <ul ...>
+ <li>...</li>
+ </ul>
+ </li>
+</ul>-<ol ...>
- <ol start="1" ...>
- <li ...>...</li>
- </ol>
-</ol>
+<ol ...>
+ <li ...>
+ ...
+ <ol start="1" ...>
+ <li ...>...</li>
+ </ol>
+ </li>
+</ol>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/assets/templates/weekly--team-report.html` around lines 8 -
11, The template contains invalid list nesting where child <ul>/<ol> tags are
direct children of other lists instead of being inside an <li> (e.g., the
repeated leading <ul data-list-bullet="true"> and the <ol ...
data-list-number="true"> that sit beside <li class="temp-li bullet2"> and <li
class="temp-li number1">); fix by moving each nested <ul> or <ol> so it is a
child of the appropriate preceding <li> (for example place the inner <ul
data-list-bullet="true"> that contains <li class="temp-li bullet2"> inside the
parent <li class="temp-li number1"> or the corresponding parent <li
class="temp-li bullet2">), remove any duplicated wrapper lists, and ensure every
<ul>/<ol> direct child is only <li> elements and any sublists are nested inside
those <li> elements (check class names like temp-li bullet2, temp-li bullet3,
and number1 to verify correct placement).
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | ||
|
|
There was a problem hiding this comment.
Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.
📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | |
| **编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范** |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/references/lark-mail-forward.md` around lines 16 - 17, The
line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken relative link; remove the erroneous
closing "**" and correct the markdown link so the link text and target point to
the actual file name in the same directory (e.g., use lark-mail-html.md or
./lark-mail-html.md) ensuring the link is valid and the bold markers are
balanced.
| > 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整 | ||
|
|
||
| ## 风格底线 | ||
|
|
||
| - **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难 | ||
| - **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>` | ||
| - **列表书写规则**:**不要**用 `<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`。 | ||
| - **正文长度自适应**:不正文长度,但要求**首屏要见到关键信息**。 |
There was a problem hiding this comment.
Fix typos in the writing guide.
Two user-facing typos in this CRITICAL guide:
- Line 11:
处于安全考虑should be出于安全考虑—处于means "to be in [a state]";出于is the correct preposition for "out of/for [a reason]". - Line 18:
不正文长度is missing a verb. From context (正文长度自适应:...,但要求首屏要见到关键信息), it should read不限制正文长度.
📝 Proposed fix
-> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
+> 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,出于安全考虑和格式适配,你输入的 HTML 可能会被自动调整
@@
-- **正文长度自适应**:不正文长度,但要求**首屏要见到关键信息**。
+- **正文长度自适应**:不限制正文长度,但要求**首屏要见到关键信息**。📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| > 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整 | |
| ## 风格底线 | |
| - **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难 | |
| - **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>` | |
| - **列表书写规则**:**不要**用 `<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`。 | |
| - **正文长度自适应**:不正文长度,但要求**首屏要见到关键信息**。 | |
| > 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,出于安全考虑和格式适配,你输入的 HTML 可能会被自动调整 | |
| ## 风格底线 | |
| - **邮件标题小于50字**: 邮件主题行 `--subject` 应控制在 50 字内,避免超长标题带来理解困难 | |
| - **多用列表、表格**:不要堆叠过长的文本段落,请擅长使用列表`<ul>` / `<ol>`或分段 `<p>` | |
| - **列表书写规则**:**不要**用 `<p>一、...</p><p>二、...</p>` 这种「中文编号 + 段落」的列表样式,"①②③"、"1) 2) 3)的机械写法也请摒弃;请擅长使用列表格式 `<ul>` / `<ol>`。 | |
| - **正文长度自适应**:不限制正文长度,但要求**首屏要见到关键信息**。 |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~15-~15: 数词与名词之间一般应存在量词,可能缺少量词。
Context: ...,你输入的 HTML 可能会被自动调整 ## 风格底线 - 邮件标题小于50字: 邮件主题行 --subject 应控制在 50 字内,避免超长标题带...
(wa5)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/references/lark-mail-html.md` around lines 11 - 18, Replace
the two typos in the guide text: change the phrase "处于安全考虑" to "出于安全考虑" (the
first sentence about the built-in HTML lint tool) and change "不正文长度" to
"不限制正文长度" (the "正文长度自适应" bullet) so the wording reads correctly and preserves
the original guidance.
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | ||
|
|
There was a problem hiding this comment.
Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.
📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/references/lark-mail-reply-all.md` around lines 16 - 17, The
line containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray trailing bold marker and a broken/duplicated relative link; remove the
extraneous "**" and ensure the Markdown link to references/lark-mail-html.md is
valid (single [text](references/lark-mail-html.md)) so the sentence reads
cleanly and the link resolves correctly in lark-mail-reply-all.md.
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify where lark-mail-html.md actually lives
fd "lark-mail-html.md" skills/lark-mail/
# Confirm the path referenced in the link does NOT exist
ls skills/lark-mail/references/references/ 2>/dev/null && echo "WARNING: nested references/ dir exists" || echo "Nested path absent (expected)"Repository: larksuite/cli
Length of output: 133
Fix malformed bold markup and broken relative link. The line has two issues: no opening ** for bold (only closing ** at end), and the link path references/lark-mail-html.md resolves to a non-existent nested directory. Since lark-mail-html.md exists in the same directory as this file, the link should be lark-mail-html.md without the references/ prefix.
Suggested fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | |
| **编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范** |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/references/lark-mail-reply.md` around lines 20 - 21, The
markdown line starting with "编辑邮件内容前 MUST 先用 Read 工具读取 ..." has malformed bold
markup (missing the opening "**") and an incorrect relative link; fix it by
adding the opening "**" before the sentence and changing the link target from
"references/lark-mail-html.md" to "lark-mail-html.md" so it points to the file
in the same directory (update the line content accordingly).
| 编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范** | ||
|
|
There was a problem hiding this comment.
Same two issues as in lark-mail-reply.md Line 20: malformed ** bold marker and broken relative link.
📝 Proposed fix
-编辑邮件内容前 MUST 先用 Read 工具读取 [references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**
+**编辑邮件内容前 MUST 先用 Read 工具读取 [lark-mail-html.md](lark-mail-html.md),其中包含邮件书写规范**🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@skills/lark-mail/references/lark-mail-send.md` around lines 15 - 16, The
sentence fragment containing "编辑邮件内容前 MUST 先用 Read 工具读取
[references/lark-mail-html.md](references/lark-mail-html.md),其中包含邮件书写规范**" has a
stray/malformed bold marker and a broken relative link; remove or correctly
place the trailing "**" and update the link target to the correct relative
filename (e.g. "lark-mail-html.md" or "./lark-mail-html.md") so the Markdown
link resolves, ensuring the text reads cleanly and the reference to
lark-mail-html.md is valid.
| } | ||
| if id, ok := blockedTags[tag]; ok { | ||
| return "error", id | ||
| } |
There was a problem hiding this comment.
Issue: <svg> and <math> tags are not in blockedTags and fall through to return "allow", "". Both are well-known XSS vectors:
<svg onload="alert(1)">— theonloadhandler gets stripped byprocessAttributes, but<svg><foreignObject><body onload="...">may bypass this depending on howgolang.org/x/net/htmlhandles SVG namespace switching.<math><mtext><table><mglyph><style><!--</style><img src onerror=...>— this is a classic mXSS (mutation XSS) pattern exploiting parser-context differences between HTML and MathML.<svg><script>alert(1)</script></svg>— the inner<script>is caught by the walker, but the SVG element itself survives and could carry other active content.
Neither <svg> nor <math> are in the PR description's tag allow-list, so allowing them by default appears unintentional.
Severity: 🚫 Must Fix
Suggested Fix: Add <svg>, <math>, and their namespace-sensitive children (<foreignObject>, <mtext>, <mglyph>, <annotation-xml>) to blockedTags. If SVG inline images are needed, they should go through <img src="data:image/svg+xml;..."> (which is already allowed, though see the separate comment on SVG data URIs).
| // don't trip the rule. | ||
| rest = strings.TrimSpace(rest) | ||
| if strings.HasPrefix(strings.ToLower(rest), "image/") { | ||
| return "ok", "" |
There was a problem hiding this comment.
Issue: data:image/svg+xml URIs pass the allow-list because the check only verifies strings.HasPrefix(rest, "image/"). SVG data URIs can contain embedded <script> tags, event handlers, and <foreignObject> — they are a well-documented XSS vector (OWASP, PortSwigger). When rendered in an <img> tag, most modern browsers sandbox SVG and block script execution, but when used in CSS background-image or other contexts, the behavior is less predictable.
Severity: 🚫 Must Fix
Suggested Fix: Either block data:image/svg+xml specifically:
if strings.HasPrefix(strings.ToLower(rest), "image/") {
if strings.HasPrefix(strings.ToLower(rest), "image/svg") {
return "error", RuleAttrDataURLBlocked
}
return "ok", ""
}Or, more conservatively, restrict data URIs to raster formats only (image/png, image/jpeg, image/gif, image/webp).
| return -1 | ||
| } | ||
| return r | ||
| }, value) |
There was a problem hiding this comment.
Issue: The control-byte stripping only removes chars < 0x20 and 0x7F, but does not strip Unicode zero-width / invisible characters that can obfuscate URL schemes:
java\u200Bscript:(zero-width space U+200B inside "javascript") → extracted scheme is"java\u200Bscript"which does NOT matchblockedURLSchemes["javascript"], so it falls to the default"warn"case instead of"error".- Similar bypasses via U+200C (ZWNJ), U+200D (ZWJ), U+FEFF (BOM), U+00A0 (NBSP), U+2028/U+2029 (line/paragraph separator), or bidi override characters (U+202A-U+202E).
Whether downstream renderers actually execute java\u200Bscript: depends on the mail client, but this is a defense-in-depth gap — the existing test TestRun_WhitespaceObfuscatedJavaScriptScheme only covers ASCII whitespace (\t, \n), not Unicode invisible chars.
Severity: 🚨 Should Fix
Suggested Fix: Extend the strings.Map filter to also strip Unicode categories Cf (format chars), Zs (space separators), and Zl/Zp (line/paragraph separators):
import "unicode"
value = strings.Map(func(r rune) rune {
if r < 0x20 || r == 0x7F {
return -1
}
if unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Zs, r) ||
unicode.Is(unicode.Zl, r) || unicode.Is(unicode.Zp, r) {
return -1
}
return r
}, value)| return "ok", "" | ||
| } | ||
| return "error", RuleAttrJSURLBlocked | ||
| case blockedURLSchemes[scheme]: |
There was a problem hiding this comment.
Issue: When a data:text/html or file: URL is blocked, the rule ID is RuleAttrJSURLBlocked ("ATTR_JS_URL_BLOCKED"). This is semantically incorrect — a data:text/html URI is not a JavaScript URL, and file: is a local file access scheme. The finding's RuleID is part of the public envelope contract (per the S2 contract reference in the package comment), so consumers pattern-matching on rule IDs will misclassify these.
Severity: 🚨 Should Fix
Suggested Fix: Introduce distinct rule IDs:
RuleAttrDataURLBlocked = "ATTR_DATA_URL_BLOCKED"
RuleAttrFileURLBlocked = "ATTR_FILE_URL_BLOCKED"And use them in the respective switch cases.
| rep.Applied = append(rep.Applied, finding) | ||
| } | ||
| rewriteWarnTag(n, tagName) | ||
| // Recurse into the rewritten node by falling through; the |
There was a problem hiding this comment.
Issue: When opts.Strict = true AND opts.AutoFix = true, the finding is classified as SeverityError and added to rep.Blocked, but rewriteWarnTag is still called — the tag is rewritten (e.g., <font> → <span>), not removed. This is inconsistent with how true error-tier tags are handled (line 131: parent.RemoveChild(n)).
In strict mode, all findings are errors. A <font> tag classified as error should arguably be removed entirely (like <script>), not rewritten to <span>. The current behavior means strict mode reports it as an error but still produces a "fixed" output, which sends mixed signals to CI consumers using --strict.
Severity: 🚨 Should Fix
Suggested Fix: When opts.Strict is true, skip the rewrite and remove the tag's children (re-parent them) or remove the tag entirely, consistent with the error-tier treatment. Or, if the current behavior is intentional (strict = "report as error but still fix"), document this explicitly in the Options.Strict godoc and add a test asserting that strict-promoted warnings are rewritten, not removed.
| · [YYYY-MM-DD]:调研完成日期(ISO 格式) | ||
| ============================================================================= | ||
| --> | ||
| <style> |
There was a problem hiding this comment.
Issue: This template relies entirely on a <style> block with CSS classes (display:flex, linear-gradient, box-shadow, gap, flex-wrap, etc.). Two problems:
- Doc inconsistency:
skill-template/domains/mail.mdline 288 states<style>blocks "会被直接删除" (will be deleted), but the linter actually passes<style>through (it's not inblockedTags). Either the doc or the code is wrong. - Server-side risk: If the Feishu mail server-side sanitizer strips
<style>blocks (as the doc claims), this template degrades to unstyled HTML — all CSS classes lose their definitions,display:flexlayouts break, and the card/gradient design disappears. All other templates correctly use only inline styles. - CSS properties outside allow-list:
box-shadow,flex,gap,linear-gradient,flex-wrapare NOT inallowedStyleProps— they only work because they're in a<style>block (bypassing inline-style filtering), which makes this template fragile.
Severity: 🚨 Should Fix
Suggested Fix: Rewrite this template to use inline styles only (like the other 4 templates), or explicitly add <style> to allowedTags and update mail.md to document that <style> is allowed. If <style> is intentionally passed through (as the linter code suggests), the domain doc must be corrected.
| // pass classifyURLValue. Lower-case canonical names. | ||
| var urlAttributes = map[string]bool{ | ||
| "href": true, | ||
| "src": true, |
There was a problem hiding this comment.
Severity: 💡 Suggestion
Issue: srcset is missing from the URL attribute list. Since <img> passes through allowedTags, an <img srcset="javascript:..."> would not have its srcset value checked by classifyURLValue. While srcset has a more complex value syntax (comma-separated URL + descriptor pairs), the javascript: prefix would still be present in the raw value.
Also, the PR description's "Inline style allow-list" mentions letter-spacing but it's not in allowedStyleProps — minor doc/code discrepancy.
Suggested Fix: Add "srcset": true to urlAttributes. For letter-spacing, either add it to allowedStyleProps or remove it from the PR description's allow-list.
| // End-to-end: +draft-create writing path emits envelope with lint fields. | ||
| // ===================================================================== | ||
|
|
||
| // TestMailDraftCreate_WritePathLintEnvelopeDefault verifies +draft-create's |
There was a problem hiding this comment.
Severity: 💡 Suggestion
Issue: The writepath E2E tests only cover +draft-create. The other 5 compose shortcuts (+send, +reply, +reply-all, +forward, +draft-edit) were all wired up with lint integration in this PR but lack dedicated writepath tests. While the shared helper functions (runWritePathLint, applyLintToEnvelope) are tested in isolation, the integration wiring in each shortcut's Execute function (flag registration, correct placement in the output pipeline, both draft/send branches) is not verified.
Suggested Fix: Add at least one E2E test per shortcut confirming the lint envelope fields appear in the output. For shortcuts with two branches (+forward/+reply/+reply-all/+send — draft-saved vs. sent), test both branches.
| "word-wrap": true, | ||
| "overflow": true, | ||
| "overflow-wrap": true, | ||
| "vertical-align": true, |
There was a problem hiding this comment.
Issue: From an email security perspective, the combination of allowed CSS properties enables several classic email abuse patterns that enterprise email gateways (Proofpoint, Mimecast, Barracuda) actively flag:
- Invisible phishing overlay:
opacity:0on an<a>tag creates a clickable but invisible link. Combined withcursor:pointer, the user clicks what they think is one element but actually hits a hidden link underneath. - Hidden content:
display:nonehides content from visual rendering while it remains in the DOM — used for spam keyword stuffing, hidden tracking URLs, or payload hiding. - Zero-size text:
font-size:0orfont-size:1pxmakes text invisible — a classic spam technique to embed keywords or hidden URLs. - Camouflaged text:
colorcan be set identical tobackground-color(e.g., white-on-white), hiding text from readers but not from the DOM. - Tracking pixel construction:
width:0;height:0orwidth:1px;height:1pxon<img>with an externalsrccreates a tracking pixel — the lint passes this through without warning.
These are not theoretical — they are documented in RFC 8058 (One-Click Unsubscribe), M3AAWG best practices, and OWASP email security guidelines.
Severity: 🚨 Should Fix
Suggested Fix: Add lint warnings (not errors — these have legitimate uses) for suspicious CSS patterns:
opacity:0oropacityvalues very close to 0display:nonefont-size:0orfont-size:1px<img>withwidthorheight<= 2px (tracking pixel heuristic)
This doesn't require blocking — just surfacing them as SeverityWarning findings so consumers and users are aware.
| for _, attr := range n.Attr { | ||
| name := strings.ToLower(attr.Key) | ||
|
|
||
| // 1. on*-handlers → always drop, error-tier. |
There was a problem hiding this comment.
Issue: The attribute scanner does not check for link text vs. href mismatch — one of the most common phishing patterns in email. Example:
<a href="https://evil-site.com">https://your-company.com/dashboard</a>The visible text looks like a legitimate internal URL, but the actual href points elsewhere. Enterprise email security gateways (Microsoft Defender for Office 365, Google Safe Browsing, Proofpoint URL Defense) specifically flag this pattern. Since this lint library aims to be a "safety floor" for the email writing path, detecting obvious URL-text mismatches would significantly strengthen its anti-phishing value.
Severity: 💡 Suggestion
Suggested Fix: When processing <a> tags, if the text content of the anchor looks like a URL (starts with http:// or https://), compare its domain with the href domain. If they differ, emit a warning finding like LINK_TEXT_HREF_DOMAIN_MISMATCH. This is a heuristic — it won't catch all phishing — but it catches the most common pattern with very low false-positive rate.
| .gradient-header { background:linear-gradient(135deg, #1a73e8, #4285f4); border-radius:12px; padding:32px; color:white; text-align:center; } | ||
| .card { background-color:white; border-radius:8px; padding:20px; margin:16px 0; box-shadow:0 1px 3px rgba(0,0,0,0.1); } | ||
| .stat-row { display:flex; gap:10px; margin:16px 0; } | ||
| .stat-card { flex:1; background-color:white; border-radius:8px; padding:14px; text-align:center; box-shadow:0 1px 3px rgba(0,0,0,0.1); } |
There was a problem hiding this comment.
Issue: display:flex and gap have zero support in Outlook (all versions — Outlook uses the Word HTML rendering engine, not a browser engine). This template's card-grid layout will collapse into a vertical stack with no gap spacing when opened in Outlook, which is the dominant email client in enterprise environments.
Specific incompatibilities in this template:
| CSS Property | Outlook | Gmail (web) | Apple Mail | Yahoo |
|---|---|---|---|---|
display:flex |
❌ | ✅ | ❌ | |
gap |
❌ | ❌ | ✅ | ❌ |
linear-gradient |
❌ | ❌ | ✅ | |
box-shadow |
❌ | ✅ | ✅ | ✅ |
border-radius |
❌ (Windows) | ✅ | ✅ | ✅ |
max-width on div |
❌ | ✅ | ✅ | ✅ |
The email industry standard for multi-column layouts is <table> with inline styles — it's ugly to write but renders consistently across all clients. The other 4 templates in this PR correctly avoid these properties.
Severity: 🚨 Should Fix
Suggested Fix: Rewrite the card-grid sections (.stat-row, .player-row) using <table> with <td> for each card. Replace linear-gradient with a solid color fallback. Move all class-based styles to inline style attributes. Use Can I Email as the compatibility reference.
| "section": true, | ||
| "article": true, | ||
| "header": true, | ||
| "footer": true, |
There was a problem hiding this comment.
Issue: Semantic HTML5 tags (<section>, <article>, <header>, <footer>, <nav>, <main>, <figure>, <figcaption>) are in allowedTags, but email clients handle them very inconsistently:
- Outlook (Word engine): renders them as inline spans with no block behavior — layout completely breaks.
- Gmail: strips
<article>,<nav>,<main>entirely; keeps<section>/<header>/<footer>but removes their semantic meaning. - Yahoo Mail: strips most semantic tags.
- AOL: strips all HTML5 semantic tags.
Email HTML is effectively a subset of HTML4 + CSS2.1. The industry best practice (per Litmus, Email on Acid, MJML framework) is to use only <div>, <span>, <table>, <p>, <h1>-<h6>, and formatting tags. Including semantic tags in the allow-list creates a false sense of safety — authors will use them thinking they're "allowed," but the rendered result in major email clients will be broken or stripped.
Severity: 💡 Suggestion
Suggested Fix: Either move <section>, <article>, <header>, <footer>, <nav>, <main>, <figure>, <figcaption> to warnAutofixTags (rewrite to <div>) or add a lint hint when these tags are used, noting that they have poor email client support. The reference doc lark-mail-html.md already doesn't mention them in any examples — implicitly acknowledging they shouldn't be used.
| } | ||
| child := parent.FirstChild | ||
| for child != nil { | ||
| next := child.NextSibling |
There was a problem hiding this comment.
Issue: The applyFeishuNativeStyles pass adds native inline styles to <img> tags — but the lint does not enforce alt attribute presence on <img>. Missing alt text is both an accessibility failure (WCAG 2.1 Level A, criterion 1.1.1) and a practical email UX problem — most email clients block external images by default and display alt text as a fallback. An <img> without alt shows as a blank rectangle or broken icon until the recipient manually loads images.
This is not a niche concern: Google Workspace, Outlook, and Apple Mail all block images by default for external senders. Enterprise security policies often enforce image blocking. Alt text is the only way recipients see anything until they click "load images."
Severity: 💡 Suggestion
Suggested Fix: In processAttributes or as a post-pass, emit a SeverityWarning finding when an <img> tag lacks an alt attribute (or has alt=""). Rule ID suggestion: IMG_MISSING_ALT_TEXT.
|
|
||
| > 请注意,邮件内容编辑相关的 shortcut 内置 HTML lint 工具,处于安全考虑和格式适配,你输入的 HTML 可能会被自动调整 | ||
|
|
||
| ## 风格底线 |
There was a problem hiding this comment.
Issue: Two inconsistencies between this doc and skill-template/domains/mail.md:
-
Subject line length: This doc says "邮件标题小于50字" (line 13), but
mail.mdsays "标题 ≤ 30 字" (line 302). These are contradictory. Industry standard is typically 30-50 chars depending on mobile vs. desktop — but the two docs in the same PR must agree. -
opacityexample (line 209 of this doc): The writing guide includes<span style="opacity:0.5">半透明文字</span>as a recommended pattern. While semi-transparent text is fine, showingopacityas a first-class recommended feature invites misuse (opacity:0for invisible content). Consider whether this example belongs in an email writing guide, given the phishing risk described in the separate security comment.
Severity: 🚨 Should Fix (for the inconsistency), 💡 Suggestion (for the opacity example)
Suggested Fix: Align both documents on one subject-length limit. If 50 is the intent, update mail.md; if 30, update this doc. For the opacity example, consider adding a note that opacity:0 will trigger a lint warning (if the invisible-content detection suggested in the security comment is implemented).
Summary
Adds an HTML lint library + Larksuite-native autofix to
lark-cli mail, plus theskills/lark-mail/skill bundle (2 reference docs, 5 HTML templates, the+lint-htmlshortcut, and writing-path lint integration across all 6 compose shortcuts).What's in this PR
1. Lint library (
shortcuts/mail/lint/)3-tier rule set:
<script>/<iframe>/<form>/<input>/<link>/<object>/<embed>),on*event handlers, andjavascript:/vbscript:/file:URLs.<font>/<center>/<marquee>/<blink>).<p>/<ul>/<ol>/<li>/<blockquote>/<a>to mail-editor native markup so AI can write the simplest HTML and still produce native-quality rendering.<style>block passthrough (server adds CSS scope class).2.
+lint-htmlshortcut (preview / CI)Read-only HTML preview tool. Default envelope returns only
cleaned_html;--show-lint-detailsadds fullwarnings[]/errors[].--strictexits non-zero on any finding (CI gate).3. Writing-path lint in the 6 compose shortcuts
+send/+draft-create/+reply/+reply-all/+forward/+draft-editbody op all run lint before drafting:lint_applied_count/original_blocked_count— always present.lint_applied[]/original_blocked[]— only with--show-lint-details.compose_hint— points AI consumers to the HTML writing guide.4.
skills/lark-mail/skill bundlereferences/lark-mail-html.md— writing rules + format primitives + template-usage flow.references/lark-mail-lint-html.md—+lint-htmlusage + return-value contract + 9 examples.SKILL.mdupdates linking the new docs and templates.5. Sealed conventions
Fixed writing conventions enforced by the lint library, the Larksuite mail-editor data model, or the upstream service-side sanitiser.
id="at-user-N"is the only hard requirement; do not writedata-user-id.http(s):/mailto:/cid:/data:image/*only.<p>/<div>/<span>/<a>/<img>/<table>(with<thead>/<tbody>/<tfoot>/<tr>/<td>/<th>/<caption>/<colgroup>/<col>) /<ul>/<ol>/<li>/<blockquote>/<h1>-<h6>/<b>/<i>/<em>/<strong>/<u>/<s>/<sub>/<sup>/<pre>/<code>/<style>.Tests
shortcuts/mail/lint/...— unit tests for every rule.shortcuts/mail/mail_lint_html_test.go—+lint-htmlenvelope contract.shortcuts/mail/mail_lint_writepath_test.go— writing-path envelope contract.+draft-createsmoke test.Test plan
go test ./shortcuts/mail/lint/... ./shortcuts/mail/...+draft-createsmoke--show-lint-detailsenvelope verified for both+lint-htmland+draft-createSummary by CodeRabbit
Release Notes
New Features
+lint-htmlcommand to locally validate and auto-fix email HTML bodies.+draft-create,+send,+reply, etc.) now automatically sanitize HTML, removing dangerous markup and fixing deprecated tags.--show-lint-detailsflag to view sanitization results across mail shortcuts.Documentation
+lint-htmlusage guide with examples of warnings vs. errors.