Skip to content

feat(frontend): add HTML attachment preview and share link generation - #872

Merged
qdaxb merged 1 commit into
wecode-ai:mainfrom
johnny0120:feat-share-address-for-html-attachment
Mar 30, 2026
Merged

qdaxb merged 1 commit into
wecode-ai:mainfrom
johnny0120:feat-share-address-for-html-attachment

Conversation

@johnny0120

@johnny0120 johnny0120 commented Mar 27, 2026 •

Copy link
Copy Markdown
Collaborator
  • Add HtmlPreview component for previewing HTML files with iframe and source view modes
  • Update FilePreview to support HTML file type detection and rendering
  • Add createAttachmentShareLink API for generating public share links
  • Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
  • Add Web Page type label and icon for HTML attachments in AttachmentCard
  • Fix preview page height to fill viewport using h-screen and h-full
  • Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments

Summary by CodeRabbit

  • New Features

    • Create and copy public share links for attachments with configurable expiration.
    • HTML preview component with toggleable Preview/Source modes and integrated share button.
  • Improvements

    • Support for .html5 and better web-page detection, labeling, and file icons.
    • Updated file preview/dialog header, share progress feedback, and synchronized HTML view modes.
    • Improved embedding/preview handling for attachments.
  • Localization

    • Added translations for sharing and HTML preview actions and messages.

@coderabbitai

coderabbitai Bot commented Mar 27, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds HTML-specific preview support (preview/source modes), a server-backed attachment public-share API/client, UI share flow with clipboard/toasts, HTML detection in preview utilities, and multiple component integrations and i18n updates.

Changes

Cohort / File(s) Summary
Attachment API & client
frontend/src/apis/attachments.ts
Added PublicShareLinkResponse, included .html5 in supported extensions/icon logic, and added createAttachmentShareLink + export on attachmentApis.
Preview classification & utils
frontend/src/components/common/FilePreview/utils.ts
Added 'html' PreviewType, helpers isHtmlMimeType/isHtmlFilename/isHtmlFile, updated isFilePreviewable and getPreviewType to detect HTML before text, and removed html/htm from generic text regex.
Html preview renderer & exports
frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx, frontend/src/components/common/FilePreview/preview-renderers/index.ts
New HtmlPreview component (preview vs source modes, controlled/uncontrolled), and re-export added in preview-renderers index.
FilePreview integration & dialogs/pages
frontend/src/components/common/FilePreview/FilePreview.tsx, .../FilePreviewDialog.tsx, .../FilePreviewPage.tsx, frontend/src/app/download/shared/page.tsx
Wire HTML preview mode props through FilePreview, add htmlIsSourceMode state and toggles, integrate useAttachmentShare (share UI/state/isSharing/shared), update header layout and icons, and wrap FilePreviewPage in div.h-screen on shared page.
Attachment sharing hook & re-exports
frontend/src/components/common/FilePreview/hooks/useAttachmentShare.ts, .../hooks/index.ts
New useAttachmentShare hook that calls createAttachmentShareLink, copies link to clipboard, shows toasts, and exposes isSharing/shared/share; re-exported in hooks index.
Attachment UI components & embedding
frontend/src/components/common/AttachmentCard.tsx, frontend/src/components/common/AttachmentEmbed.tsx, frontend/src/components/common/HtmlPreview.tsx
Pass shareToken into HTML preview paths, change label/DOM wrapper adjustments, treat html/html5 as "Web Page", and replace previous blob/iframe modal flow with server-provided text preview pipeline in HtmlPreview.
Task feature & preview controls
frontend/src/features/tasks/components/input/AttachmentPreview.tsx
Detect HTML attachments, render HTML-specific compact/expanded preview buttons that mount FilePreviewDialog, and update tooltip/label translations and icons.
i18n updates
frontend/src/i18n/locales/en/common.json, frontend/src/i18n/locales/zh-CN/common.json
Added action labels (download, share, preview, etc.), attachment.share messages, attachment.html controls, and updated upload tooltip and click-to-download/preview copy.
Backend endpoint auth
backend/app/api/endpoints/adapter/attachments.py
Changed public-share-link endpoint to fetch context via get_context_optional, require the attachment owner (user id) to create share links, and return explicit 404/403 for missing/not-owner cases.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Browser UI
  participant Dialog as FilePreviewDialog
  participant API as Attachments API
  participant Clipboard as Clipboard
  participant Toast as Toasts

  UI->>Dialog: click "Share"
  Dialog->>API: POST /api/attachments/{id}/public-share?expires_in_days=7 (+Auth token if present)
  API-->>Dialog: 200 { share_url, expires_at } or error
  Dialog->>Clipboard: navigator.clipboard.writeText(share_url)
  Clipboard-->>Dialog: copied
  Dialog->>Toast: show success ("link copied") or show error
  Note right of Dialog: update isSharing/shared state, auto-reset shared after 2s
Loading
sequenceDiagram
  participant Page as FilePreviewPage
  participant Preview as HtmlPreview
  participant Window as Browser Window
  participant Helper as openHtmlInNewTab

  Page->>Preview: provide content + isSourceMode flag
  UI->>Page: user clicks "Open in new tab"
  Page->>Window: dispatch CustomEvent 'html-preview-open-new-tab'
  Window->>Preview: event received
  Preview->>Helper: openHtmlInNewTab(content, filename)
  Helper->>Window: create Blob URL and window.open(blobUrl) or trigger download
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • qdaxb

"I hopped through bytes and windows wide,
spun little pages from content inside.
I copied a link with a twitch of my nose,
shared it in silence where bright carrot grows.
🐰✨"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding HTML attachment preview support and share link generation functionality across the frontend.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx (2)

122-124: Minor: Blob created on every render for size calculation.

new Blob([content]).size creates a new Blob object on each render. For large HTML content, this could be inefficient.

Memoize the size calculation
+  const contentSize = useMemo(() => new Blob([content]).size, [content])
+
   return (
     <div className="flex flex-col h-full bg-white dark:bg-gray-900">
       ...
         <span className="ml-2 text-text-muted">
-          ({new Blob([content]).size.toLocaleString()} bytes)
+          ({contentSize.toLocaleString()} bytes)
         </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`
around lines 122 - 124, The render currently creates a Blob on every render to
compute size (new Blob([content]).size); update the HtmlPreview component to
memoize the computed size using React's useMemo so the Blob is only created when
content changes (e.g., const size = useMemo(() => new Blob([content]).size,
[content]) and then render size.toLocaleString()). Locate this in
HtmlPreview.tsx where content is read and replace the inline new Blob creation
with the memoized size variable so large HTML doesn't recreate Blobs on each
render.

65-72: escapeHtml function is a no-op — replacements map characters to themselves.

Each .replace() call replaces a character with itself (e.g., & → & instead of & → &amp;). The function does nothing.

However, since the escaped content is rendered via JSX (<code>{escapeHtml(content)}</code>), React's built-in escaping already handles this. You can safely remove the function and use content directly, or fix it if you need the escaping for non-JSX contexts.

Option 1: Remove redundant function (recommended)
-  // Escape HTML for source view
-  const escapeHtml = (html: string): string => {
-    return html
-      .replace(/&/g, '&')
-      .replace(/</g, '<')
-      .replace(/>/g, '>')
-      .replace(/"/g, '"')
-      .replace(/'/g, '&#039;')
-  }

And on line 113:

-              <code>{escapeHtml(content)}</code>
+              <code>{content}</code>
Option 2: Fix the escape sequences (if needed elsewhere)
   const escapeHtml = (html: string): string => {
     return html
-      .replace(/&/g, '&')
-      .replace(/</g, '<')
-      .replace(/>/g, '>')
-      .replace(/"/g, '"')
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
       .replace(/'/g, '&#039;')
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`
around lines 65 - 72, The escapeHtml function is a no-op and can be removed:
delete the escapeHtml function declaration and update the usage at
<code>{escapeHtml(content)}</code> to use the raw content (e.g.,
<code>{content}</code>), also removing any unused references to escapeHtml;
alternatively, if you truly need manual escaping for non-JSX contexts, correct
the replacement mappings inside escapeHtml to map &→&amp;, <→&lt;, >→&gt;,
"→&quot;, and '→&#039; and ensure the corrected function is used where
necessary.
frontend/src/components/common/FilePreview/FilePreviewPage.tsx (2)

140-186: HTML controls section is duplicated from FilePreviewDialog.

This entire block (toggle buttons + open in new tab) is nearly identical to FilePreviewDialog. Consider extracting to a shared HtmlPreviewControls component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx` around lines
140 - 186, The HTML preview controls block in FilePreviewPage (the isHtml
conditional rendering with the toggle buttons and handleOpenInNewTab) is
duplicated from FilePreviewDialog; extract this UI into a new reusable component
(e.g., HtmlPreviewControls) that accepts props for htmlIsSourceMode,
setHtmlIsSourceMode, and handleOpenInNewTab and any i18n/titles, then replace
the duplicated JSX in both FilePreviewPage and FilePreviewDialog with
<HtmlPreviewControls .../> to remove duplication and centralize behavior.

74-103: Consider extracting shared logic with FilePreviewDialog into a custom hook.

The handleShare and handleOpenInNewTab implementations are nearly identical to those in FilePreviewDialog.tsx. This duplication could be reduced by extracting a custom hook like useAttachmentShare or useHtmlPreviewActions.

♻️ Example custom hook
// hooks/useAttachmentShare.ts
export function useAttachmentShare(attachmentId?: number, shareToken?: string) {
  const { toast } = useToast()
  const { t } = useTranslation('common')
  const [isSharing, setIsSharing] = useState(false)
  const [shared, setShared] = useState(false)

  const handleShare = async () => {
    if (!attachmentId || shareToken) return

    setIsSharing(true)
    try {
      const response = await createAttachmentShareLink(attachmentId, 7)
      await navigator.clipboard.writeText(response.share_url)
      setShared(true)
      toast({
        title: t('attachment.share.link_copied_title'),
        description: t('attachment.share.link_copied_description'),
      })
      setTimeout(() => setShared(false), 2000)
    } catch (err) {
      console.error('Failed to create share link:', err)
      toast({
        variant: 'destructive',
        title: t('attachment.share.generate_failed_title'),
        description: err instanceof Error ? err.message : t('attachment.share.retry'),
      })
    } finally {
      setIsSharing(false)
    }
  }

  return { isSharing, shared, handleShare }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx` around lines
74 - 103, Extract the duplicated share and open-in-new-tab logic into a custom
hook (e.g., useAttachmentShare or useHtmlPreviewActions) and replace the inline
implementations in FilePreviewPage.tsx and FilePreviewDialog.tsx with the hook;
move state and helpers (isSharing, shared, setShared, setIsSharing, handleShare
logic that calls createAttachmentShareLink, navigator.clipboard.writeText,
toast, t, and error handling) into the hook and export handleShare plus any
state, and for the HTML-open logic expose a handleOpenInNewTab that dispatches
the same CustomEvent('html-preview-open-new-tab') so both components import the
hook and simply call handleShare() and handleOpenInNewTab() instead of
duplicating code.
frontend/src/components/common/FilePreview/FilePreviewDialog.tsx (1)

143-189: Consider extracting the toggle button styling logic to reduce duplication.

The active/inactive state styling for the Preview/Source toggle buttons (lines 153-157, 167-171) has duplicated conditional class logic. This could be simplified.

♻️ Suggested simplification
// Helper for toggle button styling
const getToggleButtonClass = (isActive: boolean) =>
  `rounded-sm border-0 h-9 px-2 sm:px-3 text-sm font-medium gap-1.5 ${
    isActive
      ? 'bg-primary text-white shadow-sm hover:bg-primary'
      : 'bg-white text-text-primary hover:bg-white'
  }`

// Usage:
<Button
  variant="ghost"
  size="sm"
  onClick={() => setHtmlIsSourceMode(false)}
  className={getToggleButtonClass(!htmlIsSourceMode)}
  title={t('actions.preview')}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/FilePreviewDialog.tsx` around
lines 143 - 189, Extract the duplicated conditional class logic used on the two
toggle Buttons into a small helper function (e.g., getToggleButtonClass) and use
it for the className prop of the Buttons that toggle htmlIsSourceMode; update
the preview Button (onClick={() => setHtmlIsSourceMode(false)}) to call
className={getToggleButtonClass(!htmlIsSourceMode)} and the source Button
(onClick={() => setHtmlIsSourceMode(true)}) to call
className={getToggleButtonClass(htmlIsSourceMode)} so the active/inactive
styling logic is centralized and duplication in FilePreviewDialog's toggle
Buttons is removed.
frontend/src/components/common/HtmlPreview.tsx (1)

29-44: Consider adding cleanup for the async effect to prevent state updates on unmounted components.

If the component unmounts while getAttachmentPreview is in flight, calling setContent, setError, or setIsLoading will trigger a React warning in development (and potentially cause issues). This is a common pattern in async effects.

♻️ Proposed fix with cleanup flag
  useEffect(() => {
+   let isMounted = true
    const loadContent = async () => {
      try {
        setIsLoading(true)
        const response = await getAttachmentPreview(attachmentId, shareToken || undefined)
-       setContent(response.preview_text || '')
+       if (isMounted) setContent(response.preview_text || '')
      } catch (err) {
-       setError(err instanceof Error ? err.message : 'Failed to load HTML')
+       if (isMounted) setError(err instanceof Error ? err.message : 'Failed to load HTML')
      } finally {
-       setIsLoading(false)
+       if (isMounted) setIsLoading(false)
      }
    }

    loadContent()
+   return () => {
+     isMounted = false
+   }
  }, [attachmentId, shareToken])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/HtmlPreview.tsx` around lines 29 - 44, The
async effect in useEffect (function loadContent calling getAttachmentPreview)
can update state after the component unmounts; prevent this by adding a mounted
flag (e.g., let isMounted = true) or an AbortController and checking it before
calling setContent, setError, or setIsLoading, and set isMounted = false (or
abort) in the effect cleanup; apply this change inside the useEffect that
references attachmentId and shareToken so loadContent only updates state when
the component is still mounted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/apis/attachments.ts`:
- Around line 319-323: The JSDoc above the HTML_EXTENSIONS constant is malformed
due to a duplicate "/**" creating invalid comment syntax; fix it by removing the
extra "/**" so the comment starts with a single "/**" and properly documents
HTML_EXTENSIONS (the exported constant) with a single well-formed JSDoc block.

---

Nitpick comments:
In `@frontend/src/components/common/FilePreview/FilePreviewDialog.tsx`:
- Around line 143-189: Extract the duplicated conditional class logic used on
the two toggle Buttons into a small helper function (e.g., getToggleButtonClass)
and use it for the className prop of the Buttons that toggle htmlIsSourceMode;
update the preview Button (onClick={() => setHtmlIsSourceMode(false)}) to call
className={getToggleButtonClass(!htmlIsSourceMode)} and the source Button
(onClick={() => setHtmlIsSourceMode(true)}) to call
className={getToggleButtonClass(htmlIsSourceMode)} so the active/inactive
styling logic is centralized and duplication in FilePreviewDialog's toggle
Buttons is removed.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx`:
- Around line 140-186: The HTML preview controls block in FilePreviewPage (the
isHtml conditional rendering with the toggle buttons and handleOpenInNewTab) is
duplicated from FilePreviewDialog; extract this UI into a new reusable component
(e.g., HtmlPreviewControls) that accepts props for htmlIsSourceMode,
setHtmlIsSourceMode, and handleOpenInNewTab and any i18n/titles, then replace
the duplicated JSX in both FilePreviewPage and FilePreviewDialog with
<HtmlPreviewControls .../> to remove duplication and centralize behavior.
- Around line 74-103: Extract the duplicated share and open-in-new-tab logic
into a custom hook (e.g., useAttachmentShare or useHtmlPreviewActions) and
replace the inline implementations in FilePreviewPage.tsx and
FilePreviewDialog.tsx with the hook; move state and helpers (isSharing, shared,
setShared, setIsSharing, handleShare logic that calls createAttachmentShareLink,
navigator.clipboard.writeText, toast, t, and error handling) into the hook and
export handleShare plus any state, and for the HTML-open logic expose a
handleOpenInNewTab that dispatches the same
CustomEvent('html-preview-open-new-tab') so both components import the hook and
simply call handleShare() and handleOpenInNewTab() instead of duplicating code.

In
`@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`:
- Around line 122-124: The render currently creates a Blob on every render to
compute size (new Blob([content]).size); update the HtmlPreview component to
memoize the computed size using React's useMemo so the Blob is only created when
content changes (e.g., const size = useMemo(() => new Blob([content]).size,
[content]) and then render size.toLocaleString()). Locate this in
HtmlPreview.tsx where content is read and replace the inline new Blob creation
with the memoized size variable so large HTML doesn't recreate Blobs on each
render.
- Around line 65-72: The escapeHtml function is a no-op and can be removed:
delete the escapeHtml function declaration and update the usage at
<code>{escapeHtml(content)}</code> to use the raw content (e.g.,
<code>{content}</code>), also removing any unused references to escapeHtml;
alternatively, if you truly need manual escaping for non-JSX contexts, correct
the replacement mappings inside escapeHtml to map &→&amp;, <→&lt;, >→&gt;,
"→&quot;, and '→&#039; and ensure the corrected function is used where
necessary.

In `@frontend/src/components/common/HtmlPreview.tsx`:
- Around line 29-44: The async effect in useEffect (function loadContent calling
getAttachmentPreview) can update state after the component unmounts; prevent
this by adding a mounted flag (e.g., let isMounted = true) or an AbortController
and checking it before calling setContent, setError, or setIsLoading, and set
isMounted = false (or abort) in the effect cleanup; apply this change inside the
useEffect that references attachmentId and shareToken so loadContent only
updates state when the component is still mounted.
🪄 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: 9a0a89da-af65-488e-b75a-cc2e9b7df402

📥 Commits

Reviewing files that changed from the base of the PR and between 7a9841d and 77f9f68.

📒 Files selected for processing (14)
  • frontend/src/apis/attachments.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/i18n/locales/zh-CN/common.json

Comment thread frontend/src/apis/attachments.ts Resolved
@johnny0120
johnny0120 force-pushed the feat-share-address-for-html-attachment branch from 77f9f68 to 896d2c6 Compare March 27, 2026 09:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx (1)

1-10: Use a kebab-case filename for this TSX module.

HtmlPreview.tsx breaks the repo TSX filename convention and makes this new renderer an outlier in the frontend surface.

As per coding guidelines, **/*.{tsx,jsx}: Component names use PascalCase, files use kebab-case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`
around lines 1 - 10, The file name HtmlPreview.tsx violates the repo convention;
rename the module file to kebab-case (html-preview.tsx) while keeping the React
component name (HtmlPreview) and its default export unchanged, then update all
imports referencing HtmlPreview (e.g., from preview-renderers/HtmlPreview to
preview-renderers/html-preview) so consumers find the module; ensure any
build/alias references in the FilePreview registry or dynamic imports are
updated to the new filename.
frontend/src/components/common/FilePreview/utils.ts (1)

28-35: Extract the HTML matcher into a shared helper.

isFilePreviewable() and getPreviewType() now repeat the same HTML MIME/extension logic. One future edit can make them drift and silently reintroduce classification mismatches.

♻️ Suggested refactor
+const HTML_FILE_RE = /\.(html|htm|html5)$/i
+
+const isHtmlPreviewFile = (mimeType: string, filename: string): boolean =>
+  mimeType === 'text/html' ||
+  mimeType === 'application/xhtml+xml' ||
+  HTML_FILE_RE.test(filename)
+
 export function isFilePreviewable(mimeType: string, filename: string): boolean {
   // Image types
   if (mimeType.startsWith('image/')) return true
@@
   // HTML files
-  if (
-    mimeType === 'text/html' ||
-    mimeType === 'application/xhtml+xml' ||
-    filename.match(/\.(html|htm|html5)$/i)
-  ) {
+  if (isHtmlPreviewFile(mimeType, filename)) {
     return true
   }
@@
 export function getPreviewType(mimeType: string, filename: string): PreviewType {
@@
   // HTML files - render in preview mode
-  if (
-    mimeType === 'text/html' ||
-    mimeType === 'application/xhtml+xml' ||
-    filename.match(/\.(html|htm|html5)$/i)
-  ) {
+  if (isHtmlPreviewFile(mimeType, filename)) {
     return 'html'
   }

Also applies to: 74-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/utils.ts` around lines 28 - 35,
Extract the repeated HTML detection logic into a single helper (e.g., isHtmlFile
or matchesHtmlMime) and use it from both isFilePreviewable and getPreviewType to
avoid duplication; specifically, move the mimeType checks (mimeType ===
'text/html' || mimeType === 'application/xhtml+xml') and the filename regex
(/\.(html|htm|html5)$/i) into that helper, replace the inline checks at the HTML
branch inside isFilePreviewable and the corresponding HTML branch in
getPreviewType to call the new helper, and update any imports/exports in
utils.ts so both functions reference the shared matcher.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`:
- Around line 44-52: The iframe currently renders untrusted HTML via doc.write
in the useEffect (iframeRef) and is created with "allow-scripts
allow-same-origin", which breaks sandboxing; remove "allow-same-origin" from the
iframe sandbox attribute (and preferably also remove "allow-scripts" or add a
strict CSP) and update openHtmlInNewTab to avoid executing the blob URL (change
to download the blob or open a safe about:blank with sanitized content);
alternatively sanitize content with DOMPurify before writing into the iframe in
the useEffect and in openHtmlInNewTab so injection cannot run privileged
scripts.

In `@frontend/src/components/common/HtmlPreview.tsx`:
- Around line 34-35: The component currently calls getAttachmentPreview(...) and
unconditionally does setContent(response.preview_text || ''), which converts a
missing preview into a blank iframe; change HtmlPreview.tsx to inspect
response.preview_type and response.preview_text from getAttachmentPreview before
calling setContent: if response.preview_type === 'none' or response.preview_text
is null/undefined, set the component into the "unavailable" state (e.g., set a
flag like isPreviewAvailable = false or clear content to null) and avoid
injecting an empty string into the iframe; otherwise call
setContent(response.preview_text) so only real HTML previews are rendered.
Ensure you reference response.preview_type and response.preview_text when
updating the rendering logic.
- Around line 60-61: HtmlPreview currently uses HtmlPreviewBase which writes
user HTML into an iframe via document.write and sets sandbox="allow-scripts
allow-same-origin allow-popups allow-forms", enabling stored-XSS; remove or
restrict those permissions and stop using document.write with same-origin
iframe. Update HtmlPreviewBase to (1) remove allow-scripts and allow-same-origin
from the iframe sandbox attribute (keep only minimal needed, e.g.,
allow-popups/allow-forms only if essential), (2) stop injecting content with
document.write into a same-origin iframe and instead load preview content via a
blob: or data: URL assigned to the iframe src to create a unique origin, and (3)
make preview-mode opt-in by defaulting to source-only view and requiring an
explicit user action to render the live preview.
- Around line 31-44: Reset transient error and prevent stale responses in the
loadContent async effect: at the start of loadContent call setError(null) and
setIsLoading(true) to clear previous error/loading state, and introduce a
cancellation guard (e.g., a local `let cancelled = false` or AbortController)
scoped to the effect so that when attachmentId or shareToken changes the cleanup
sets cancelled = true; after awaiting getAttachmentPreview(attachmentId,
shareToken || undefined) check the guard before calling setContent, setError or
setIsLoading to avoid overwriting newer requests. Reference functions/state:
loadContent, getAttachmentPreview, setIsLoading, setContent, setError, and
props/vars attachmentId and shareToken.

In `@frontend/src/i18n/locales/en/common.json`:
- Line 1320: The "upload_tooltip" translation string currently lists supported
file types but omits HTML/Markdown and the new HTML preview behavior; update the
"upload_tooltip" value (key: "upload_tooltip" in common.json) to include HTML
and Markdown as supported formats and optionally mention that HTML/Markdown
files will be rendered with an HTML preview, keeping the existing size
placeholder {{size}} intact and preserving newline formatting.

In `@frontend/src/i18n/locales/zh-CN/common.json`:
- Line 1314: Update the "upload_tooltip" translation (key: upload_tooltip in
common.json) to explicitly mention that HTML files are supported and
previewable; locate the JSON entry for upload_tooltip and modify its text to
include "HTML" (e.g., add "HTML" to the supported types list or a phrase like
"支持HTML并可预览") while preserving the existing placeholder {{size}} and newline
structure.

---

Nitpick comments:
In
`@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`:
- Around line 1-10: The file name HtmlPreview.tsx violates the repo convention;
rename the module file to kebab-case (html-preview.tsx) while keeping the React
component name (HtmlPreview) and its default export unchanged, then update all
imports referencing HtmlPreview (e.g., from preview-renderers/HtmlPreview to
preview-renderers/html-preview) so consumers find the module; ensure any
build/alias references in the FilePreview registry or dynamic imports are
updated to the new filename.

In `@frontend/src/components/common/FilePreview/utils.ts`:
- Around line 28-35: Extract the repeated HTML detection logic into a single
helper (e.g., isHtmlFile or matchesHtmlMime) and use it from both
isFilePreviewable and getPreviewType to avoid duplication; specifically, move
the mimeType checks (mimeType === 'text/html' || mimeType ===
'application/xhtml+xml') and the filename regex (/\.(html|htm|html5)$/i) into
that helper, replace the inline checks at the HTML branch inside
isFilePreviewable and the corresponding HTML branch in getPreviewType to call
the new helper, and update any imports/exports in utils.ts so both functions
reference the shared matcher.
🪄 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: b7f14ece-d09d-426a-a753-4d1cd7427b44

📥 Commits

Reviewing files that changed from the base of the PR and between 77f9f68 and 896d2c6.

📒 Files selected for processing (14)
  • frontend/src/apis/attachments.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/apis/attachments.ts

@johnny0120
johnny0120 force-pushed the feat-share-address-for-html-attachment branch from 896d2c6 to 083859e Compare March 27, 2026 11:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx (1)

123-137: ⚠️ Potential issue | 🔴 Critical

Don't open untrusted HTML as a same-origin blob: page.

window.open(URL.createObjectURL(new Blob([content], { type: 'text/html' })), '_blank') runs the attachment outside the iframe sandbox. Because the blob URL is minted by the app, the new tab gets the app's origin and the uploaded HTML can execute arbitrary same-origin JS there. Please keep this flow sandboxed or fall back to download / sanitized rendering on an isolated origin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`
around lines 123 - 137, The current openHtmlInNewTab function creates a
same-origin blob URL and calls window.open(url, '_blank'), which lets untrusted
HTML run with the app's origin; change this to avoid opening untrusted content
as same-origin: either always fall back to a download (create and click the
anchor using url and filename like the existing fallback) or route opening
through a dedicated isolated viewer origin/sandboxed viewer page; specifically
modify openHtmlInNewTab: stop calling window.open(url, '_blank') for raw HTML
content, and instead (a) use the existing anchor download flow when content is
untrusted, or (b) if you must preview, open a known safe viewer page (on an
isolated origin or static viewer path) and pass the blob URL or content to it so
it renders inside a sandboxed iframe there; ensure you still revoke the object
URL (URL.revokeObjectURL) after use.
🧹 Nitpick comments (1)
frontend/src/features/tasks/components/input/AttachmentPreview.tsx (1)

376-382: Guard these inline buttons against form submission.

These plain <button> elements do not declare type="button". If AttachmentPreview is rendered inside the task composer form, clicking preview/download will submit the form instead of only opening the dialog or downloading the file. Adding type="button" is a safe no-op otherwise.

Suggested fix
-              <button onClick={handlePreviewClick} className="ml-2 text-link hover:underline">
+              <button
+                type="button"
+                onClick={handlePreviewClick}
+                className="ml-2 text-link hover:underline"
+              >
                 点击预览
               </button>
               {showDownload && (
-                <button onClick={handleDownload} className="ml-2 text-link hover:underline">
+                <button
+                  type="button"
+                  onClick={handleDownload}
+                  className="ml-2 text-link hover:underline"
+                >
                   下载
                 </button>
               )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/tasks/components/input/AttachmentPreview.tsx` around
lines 376 - 382, The inline <button> elements in AttachmentPreview are missing
an explicit type and can therefore submit a surrounding form; update the two
buttons that call handlePreviewClick and handleDownload inside the
AttachmentPreview component to include type="button" so clicks only trigger the
preview/download handlers and never submit the form. Locate the JSX for the
"点击预览" and conditional "下载" buttons and add type="button" to each.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/common/FilePreview/FilePreviewDialog.tsx`:
- Around line 130-133: The custom close button replacing the dialog's built-in
control (used alongside DialogContent with hideCloseButton in FilePreviewDialog)
lacks an accessible name; update the replacement button element(s) (the custom
close button rendered near the DialogContent usages at ~130 and ~227) to include
an explicit accessible name such as adding aria-label="Close" (and optionally a
title prop) so screen readers can announce it; ensure the click handler and
styling remain unchanged and apply the same change to both instances.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx`:
- Around line 74-98: The component currently uses shareToken as a proxy for
ownership inside handleShare (and the similar block at lines 188-210); instead,
add an explicit prop like canShare or isOwner to FilePreviewPage and use that
boolean to gate sharing (e.g., return early from handleShare if !canShare),
update all UI/disable states accordingly, and pass the new prop from the parent.
Also update the server-side POST /public-share endpoint to enforce the same
owner-only check (replace the current authorization via
_get_attachment_context(...) with an explicit owner/canShare authorization
check) so non-owners cannot mint public links; keep referencing
createAttachmentShareLink and handleShare to locate the client-side changes and
POST /public-share and _get_attachment_context to locate the server-side
changes.

In
`@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`:
- Around line 61-67: Remove the unused escapeHtml helper in HtmlPreview.tsx and
stop pre-encoding entities; instead render the raw attachment text content
directly in the JSX expression so React will perform proper escaping. Locate the
escapeHtml function and any places it is invoked (e.g., inside the HtmlPreview
component render or helpers) and replace the usage with the original content
variable (or prop) rendered as {content} (do not use dangerouslySetInnerHTML).
Ensure no other code path expects pre-escaped strings and update tests/consumers
accordingly.

In `@frontend/src/features/tasks/components/input/AttachmentPreview.tsx`:
- Around line 336-347: Replace the hardcoded Chinese strings in
AttachmentPreview.tsx with localized common keys: change title="预览" to use the
i18n/common key for actions.preview, title="下载" (on the download Button that
calls handleDownload) to actions.download, and any tooltip/text "点击预览" in the
HTML preview flow (also at the block around lines 376-382) to common key
attachment.click_to_preview; locate the Buttons and the element showing the
click-to-preview text (references: the Button that renders an Eye icon, the
download Button with onClick={handleDownload}, and the HTML-preview node) and
use the existing translation helper (the project's common translations accessor)
to return translated strings instead of hardcoded Chinese.

---

Duplicate comments:
In
`@frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx`:
- Around line 123-137: The current openHtmlInNewTab function creates a
same-origin blob URL and calls window.open(url, '_blank'), which lets untrusted
HTML run with the app's origin; change this to avoid opening untrusted content
as same-origin: either always fall back to a download (create and click the
anchor using url and filename like the existing fallback) or route opening
through a dedicated isolated viewer origin/sandboxed viewer page; specifically
modify openHtmlInNewTab: stop calling window.open(url, '_blank') for raw HTML
content, and instead (a) use the existing anchor download flow when content is
untrusted, or (b) if you must preview, open a known safe viewer page (on an
isolated origin or static viewer path) and pass the blob URL or content to it so
it renders inside a sandboxed iframe there; ensure you still revoke the object
URL (URL.revokeObjectURL) after use.

---

Nitpick comments:
In `@frontend/src/features/tasks/components/input/AttachmentPreview.tsx`:
- Around line 376-382: The inline <button> elements in AttachmentPreview are
missing an explicit type and can therefore submit a surrounding form; update the
two buttons that call handlePreviewClick and handleDownload inside the
AttachmentPreview component to include type="button" so clicks only trigger the
preview/download handlers and never submit the form. Locate the JSX for the
"点击预览" and conditional "下载" buttons and add type="button" to each.
🪄 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: aaec810c-a3d8-4018-8ef2-67e50968cb89

📥 Commits

Reviewing files that changed from the base of the PR and between 896d2c6 and 083859e.

📒 Files selected for processing (14)
  • frontend/src/apis/attachments.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/utils.ts

@johnny0120
johnny0120 force-pushed the feat-share-address-for-html-attachment branch from 083859e to f2299e0 Compare March 30, 2026 02:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/src/components/common/FilePreview/FilePreviewDialog.tsx (1)

202-211: Consider using variant="primary" for the Download button.

FilePreviewPage.tsx uses variant="primary" for Download (line 197), but this dialog uses variant="outline". For consistency, consider aligning the variants. Based on learnings, "Primary action buttons in dialogs MUST use variant="primary"".

♻️ Suggested change
             <Button
-              variant="outline"
+              variant="primary"
               size="sm"
               onClick={handleDownload}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/FilePreviewDialog.tsx` around
lines 202 - 211, The Download button in FilePreviewDialog.tsx uses
variant="outline" while the page variant and dialog primary-action convention
require variant="primary"; update the Button element that renders the Download
action (the one with onClick={handleDownload} and the Download icon) to use
variant="primary" so it matches FilePreviewPage.tsx and the dialog
primary-action standard, keeping the same size, className and title props
unchanged.
frontend/src/components/common/FilePreview/FilePreviewPage.tsx (1)

77-101: Consider extracting shared logic.

The handleShare function is duplicated between FilePreviewDialog.tsx and FilePreviewPage.tsx. Consider extracting this into a custom hook (e.g., useAttachmentShare) to reduce duplication.

Also applies to: 78-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx` around lines
77 - 101, The handleShare logic duplicated in FilePreviewPage.tsx and
FilePreviewDialog.tsx should be extracted into a reusable custom hook (e.g.,
useAttachmentShare) to avoid duplication: create useAttachmentShare that accepts
the dependencies (attachmentId, canShare, and optional expiryDays) and exposes
isSharing, shared, and an async share() function that internally calls
createAttachmentShareLink, writes to navigator.clipboard, updates shared state,
shows toast messages, and resets shared after timeout; then replace the
handleShare implementations in both FilePreviewPage and FilePreviewDialog by
calling the hook and wiring its share() to the UI while preserving calls to
setIsSharing/setShared/toast behavior and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx`:
- Around line 206-216: The close Button inside FilePreviewPage.tsx is an
icon-only control (rendered when onClose is present) and lacks an accessible
name; update the Button component where onClose is used (the Button rendering
the X icon) to include an aria-label (e.g., aria-label={t('actions.close')} or
similar) so screen readers announce the button; ensure you use the same
localized label used elsewhere (FilePreviewDialog.tsx) for consistency.

---

Nitpick comments:
In `@frontend/src/components/common/FilePreview/FilePreviewDialog.tsx`:
- Around line 202-211: The Download button in FilePreviewDialog.tsx uses
variant="outline" while the page variant and dialog primary-action convention
require variant="primary"; update the Button element that renders the Download
action (the one with onClick={handleDownload} and the Download icon) to use
variant="primary" so it matches FilePreviewPage.tsx and the dialog
primary-action standard, keeping the same size, className and title props
unchanged.

In `@frontend/src/components/common/FilePreview/FilePreviewPage.tsx`:
- Around line 77-101: The handleShare logic duplicated in FilePreviewPage.tsx
and FilePreviewDialog.tsx should be extracted into a reusable custom hook (e.g.,
useAttachmentShare) to avoid duplication: create useAttachmentShare that accepts
the dependencies (attachmentId, canShare, and optional expiryDays) and exposes
isSharing, shared, and an async share() function that internally calls
createAttachmentShareLink, writes to navigator.clipboard, updates shared state,
shows toast messages, and resets shared after timeout; then replace the
handleShare implementations in both FilePreviewPage and FilePreviewDialog by
calling the hook and wiring its share() to the UI while preserving calls to
setIsSharing/setShared/toast behavior and error handling.
🪄 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: 5d67c454-6878-43e0-bb8e-933ebe7dd878

📥 Commits

Reviewing files that changed from the base of the PR and between 083859e and f2299e0.

📒 Files selected for processing (15)
  • backend/app/api/endpoints/adapter/attachments.py
  • frontend/src/apis/attachments.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/i18n/locales/en/common.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx

- Add HtmlPreview component for previewing HTML files with iframe and source view modes
- Update FilePreview to support HTML file type detection and rendering
- Add createAttachmentShareLink API for generating public share links
- Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
- Add Web Page type label and icon for HTML attachments in AttachmentCard
- Fix preview page height to fill viewport using h-screen and h-full
- Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments
@johnny0120
johnny0120 force-pushed the feat-share-address-for-html-attachment branch from f2299e0 to 1a05105 Compare March 30, 2026 02:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/components/common/FilePreview/hooks/useAttachmentShare.ts (1)

56-80: Consider adding cleanup for the setTimeout.

The setTimeout on line 69 could cause a state update on an unmounted component if the user navigates away within 2 seconds of sharing. While React 18+ handles this more gracefully, it's still best practice to clean up timers.

♻️ Proposed refactor using useRef for cleanup
+import { useState, useCallback, useRef, useEffect } from 'react'
 
 export function useAttachmentShare({
   attachmentId,
   canShare,
   expiresInDays = 7,
 }: UseAttachmentShareOptions): UseAttachmentShareReturn {
   const { toast } = useToast()
   const { t } = useTranslation('common')
   const [isSharing, setIsSharing] = useState(false)
   const [shared, setShared] = useState(false)
+  const timeoutRef = useRef<NodeJS.Timeout | null>(null)
+
+  // Cleanup timeout on unmount
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) {
+        clearTimeout(timeoutRef.current)
+      }
+    }
+  }, [])
 
   const share = useCallback(async () => {
     if (!attachmentId || !canShare) return
 
     setIsSharing(true)
     try {
       const response = await createAttachmentShareLink(attachmentId, expiresInDays)
       await navigator.clipboard.writeText(response.share_url)
       setShared(true)
       toast({
         title: t('attachment.share.link_copied_title'),
         description: t('attachment.share.link_copied_description'),
       })
       // Reset copied state after 2 seconds
-      setTimeout(() => setShared(false), 2000)
+      if (timeoutRef.current) {
+        clearTimeout(timeoutRef.current)
+      }
+      timeoutRef.current = setTimeout(() => setShared(false), 2000)
     } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/FilePreview/hooks/useAttachmentShare.ts`
around lines 56 - 80, The share function in useAttachmentShare sets a setTimeout
to reset shared state but never clears it, risking a state update after unmount;
fix by storing the timeout id in a ref (e.g., timeoutRef) inside
useAttachmentShare, clear any existing timeout before setting a new one, and add
a useEffect cleanup that clears timeoutRef.current on unmount; update references
in the share function (and any related state setters) to use the ref so timers
are always cancelled when the component unmounts or a new share starts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/src/components/common/FilePreview/hooks/useAttachmentShare.ts`:
- Around line 56-80: The share function in useAttachmentShare sets a setTimeout
to reset shared state but never clears it, risking a state update after unmount;
fix by storing the timeout id in a ref (e.g., timeoutRef) inside
useAttachmentShare, clear any existing timeout before setting a new one, and add
a useEffect cleanup that clears timeoutRef.current on unmount; update references
in the share function (and any related state setters) to use the ref so timers
are always cancelled when the component unmounts or a new share starts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d367e39a-8e25-4d0d-affb-a847f74bf3e1

📥 Commits

Reviewing files that changed from the base of the PR and between f2299e0 and 1a05105.

📒 Files selected for processing (17)
  • backend/app/api/endpoints/adapter/attachments.py
  • frontend/src/apis/attachments.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/components/common/AttachmentCard.tsx
  • frontend/src/components/common/AttachmentEmbed.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
  • frontend/src/components/common/FilePreview/FilePreviewPage.tsx
  • frontend/src/components/common/FilePreview/hooks/index.ts
  • frontend/src/components/common/FilePreview/hooks/useAttachmentShare.ts
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/components/common/FilePreview/utils.ts
  • frontend/src/components/common/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (4)
  • frontend/src/components/common/FilePreview/preview-renderers/index.ts
  • frontend/src/app/download/shared/page.tsx
  • frontend/src/i18n/locales/en/common.json
  • frontend/src/components/common/FilePreview/FilePreviewDialog.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/src/components/common/AttachmentEmbed.tsx
  • backend/app/api/endpoints/adapter/attachments.py
  • frontend/src/components/common/FilePreview/preview-renderers/HtmlPreview.tsx
  • frontend/src/features/tasks/components/input/AttachmentPreview.tsx
  • frontend/src/components/common/FilePreview/FilePreview.tsx
  • frontend/src/components/common/HtmlPreview.tsx

@qdaxb
qdaxb merged commit 5bcaf53 into wecode-ai:main Mar 30, 2026
20 checks passed
FicoHu pushed a commit to FicoHu/Wegent that referenced this pull request Apr 1, 2026
…wecode-ai#872)

- Add HtmlPreview component for previewing HTML files with iframe and source view modes
- Update FilePreview to support HTML file type detection and rendering
- Add createAttachmentShareLink API for generating public share links
- Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
- Add Web Page type label and icon for HTML attachments in AttachmentCard
- Fix preview page height to fill viewport using h-screen and h-full
- Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments
FicoHu pushed a commit to FicoHu/Wegent that referenced this pull request Apr 1, 2026
…wecode-ai#872)

- Add HtmlPreview component for previewing HTML files with iframe and source view modes
- Update FilePreview to support HTML file type detection and rendering
- Add createAttachmentShareLink API for generating public share links
- Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
- Add Web Page type label and icon for HTML attachments in AttachmentCard
- Fix preview page height to fill viewport using h-screen and h-full
- Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments
qdaxb added a commit that referenced this pull request Apr 1, 2026
* feat: add workspace archive and recovery for Pod deletion

Implement Pod recovery feature that allows users to continue conversations
after executor Pods are deleted:

- Add ArchiveInfo schema to store archive metadata in Task.status.archive
- Add workspace_archive service for MinIO presigned URL generation
- Add archive/restore endpoints in executor and executor_manager
- Add ExecutorRecoveryService for Pod recreation with workspace restoration
- Add skip_git_clone support to prevent git clone conflicts during restore
- Add Celery Beat scheduled task for expired archive cleanup (30-day retention)
- Integrate archive workflow in executor_job before Pod deletion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backend): add missing minio dependency

Add minio package to backend dependencies for workspace archive storage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(admin): add created_at field to device monitor list (#846)

- Add created_at field to AdminDeviceInfo schema in backend
- Add created_at to AdminDeviceInfo TypeScript interface
- Display creation time in device list UI
- Add i18n translations for created_at column (zh-CN and en)

Co-authored-by: yunpeng7 <yunpeng7@staff.weibo.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* perf: use async heartbeat (#849)

* fix(chat_shell): prevent orphaned tool call IDs in group chat history (#852)

* fix(chat_shell): prevent tool call group splitting in group chat history truncation

_truncate_history blindly sliced the flat message list, which could
split assistant+tool_calls and their tool response messages across the
truncation boundary. This produced orphaned function_call_output items
without matching function_call entries in the OpenAI Responses API
format, causing 'No tool call found for function call output' errors.

Add _group_messages() to treat assistant+tool_calls+tool responses as
atomic units, and modify _truncate_history() to truncate by groups
instead of individual messages.

* fix(chat_shell): ensure tool call group integrity across all compression paths

Extract group_tool_call_messages() into shared messages/utils.py and
apply atomic group handling to:
- HistoryTruncationStrategy.compress(): adjust first/last boundaries
  to avoid splitting tool call groups, add post-removal orphan cleanup
- _force_compression_to_target(): Step 2 removes middle groups instead
  of individual messages, Step 5 keeps first/last groups
- Refactor loader.py to import shared utility instead of inline helper

All 95 related tests pass. Comprehensive audit confirms no other code
paths in chat_shell break tool call group integrity.

* test(chat_shell): add tool-call group truncation regressions

Add regression coverage for preserving assistant tool calls with their matching tool results across group-chat history truncation, history compression boundaries, and forced compression fallback paths.

* fix: 在里面智能体的创建人和机器人的创建人不是一个人时会报错 (#853)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 在里面智能体的创建人和机器人的创建人不是一个人时会报错

* fix: 迁移其他的sql到kindReader

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fail fast on invalid tool call IDs across chat protocols (#854)

* fix: 订阅任务执行间隔调整 & trace http接口的匹配规则调整 (#855)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: telemetry的匹配规则调整

* fix: 订阅任务执行间隔调整

* fix: 调整逻辑

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: sanitize PyInstaller LD_LIBRARY_PATH for child processes (#859)

PyInstaller's bootloader prepends its extraction directory (/tmp/_MEIxxxxxx)
to LD_LIBRARY_PATH, causing child processes (Claude Code, shell commands)
to load wrong shared libraries (e.g., libtinfo.so.6).

Extract `sanitize_ld_library_path()` into platform_compat and apply it in
both local_mode_strategy (Claude SDK subprocess) and extension_handler
(extension script subprocess) to restore the original library path.

See: https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html

Co-authored-by: sifang <sifang@staff.weibo.com>

* refactor(rag): move internal retrieve routing into backend (#847)

* refactor(rag): move internal retrieve routing into backend

* refactor(rag): move retrieve persistence into backend

* refactor(rag): move kb head persistence into backend

* fix(rag): tighten backend retrieve routing

* refactor(rag): simplify token estimation fallback

* refactor(rag): batch route token estimation

* fix(rag): token estimator decimal

* fix(rag): harden kb read persistence

* frontend: show 'indexing' status for documents pending indexing (#858)

* fix(frontend): show 'indexing' status for documents pending indexing

When a document is uploaded, is_active is false while indexing is in progress.
Previously, this showed 'index unavailable' which was misleading.

Now documents with status='enabled' but is_active=false will show 'indexing'
instead of 'index unavailable', matching the existing translation key.

Changes:
- Updated compact mode status indicator (line 247)
- Updated normal mode status badge (line 461)

Fixes: knowledge base document shows 'index unavailable' during indexing

* fix(i18n): update Developer role description in Chinese locale

* fix: 修复订阅器中脏数据导致的无法展示的问题 (#860)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复订阅器中脏数据导致的无法展示的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* feat(backend): add knowledge base RAG support to /v1/responses API (#844)

* feat(backend): add knowledge base RAG support to /v1/responses API

Implement knowledge base Q&A functionality for the OpenAI-compatible
/v1/responses endpoint with the following changes:

Schema Changes:
- Add knowledge_base_names field to WegentTool schema
- Support namespace#name format for KB specification

New Services:
- KnowledgeBaseNameResolver: resolves KB display names to IDs with
  permission checking via TaskKnowledgeBaseService
- KnowledgeBaseContextCreator: creates SubtaskContext records for
  API-specified KBs to integrate with existing RAG pipeline

Integration:
- Extend parse_wegent_tools() to handle knowledge_base tool type
- Add parse_knowledge_base_name() for namespace#name parsing
- Modify build_execution_request() to accept knowledge_base_names
- Add _create_kb_contexts_from_api_request() for KB context injection
- Update openapi_responses.py endpoints to pass KB names parameter

Testing:
- Add comprehensive unit tests for KB name resolution
- Add tests for KB context creation
- Add tests for helper functions with KB support
- All 32 new tests passing

The implementation reuses the existing RAG flow through
prepare_contexts_for_chat() and KnowledgeBaseTool, ensuring
consistency with the web UI behavior.

* refactor(kb_resolver): optimize database query complexity from O(n) to O(m)

- Add batch_get_knowledge_base_by_names() to TaskKnowledgeBaseService for efficient batch queries
- Extract _check_kb_access_permission() for unified permission checking logic
- Update kb_resolver to use service methods instead of direct queries
- Reduce DB queries from O(n) to O(m) where m is unique namespace count
- Update tests to mock service methods instead of internal functions

All 15 tests pass.

* refactor(backend): optimize KB permission check in kb_resolver

Simplify permission checking by using get_all_knowledge_bases_grouped()
which already returns all KBs accessible to the user.

Changes:
- Replace _batch_query_kbs() and _check_kb_access_permission() with
  _get_accessible_kb_lookup() that builds a lookup dict from grouped results
- Remove unused resolve_single() method and batch_get_knowledge_base_by_names()
- Update tests to mock KnowledgeService.get_all_knowledge_bases_grouped()

This reduces database queries and simplifies the permission check logic.

* feat(backend): integrate knowledge base context creation into unified AI trigger

- Add knowledge_base_names parameter to build_execution_request for OpenAPI v1/responses
- Create _create_kb_contexts_from_api_request helper to process KB names from API requests
- Integrate KB context processing into unified trigger flow
- Add KnowledgeBaseContextCreator class for creating SubtaskContext records
- Add comprehensive unit tests for KnowledgeBaseContextCreator

This enables RAG functionality for OpenAPI v1/responses endpoint by creating
SubtaskContext records that are processed by the existing context pipeline.

* fix(chat): propagate HTTPException from KnowledgeBaseNameResolver in unified trigger

- Add HTTPException import from fastapi
- Catch and re-raise HTTPException explicitly in _create_kb_contexts_from_api_request
- Keep generic exception handling for non-HTTP exceptions to preserve best-effort behavior
- This ensures 403/404 errors from KB resolution are properly propagated to callers
  instead of being swallowed and logged as warnings

* fix(chat_shell): use model_type for provider detection instead of model_id (#861)

- Extract shared detect_provider() into models/providers.py with
  PROVIDER_ALIASES as single source of truth
- model_type (env.model) is authoritative; raise ValueError on unknown
- Remove _PROVIDER_ID_PATTERNS model_id prefix fallback (same model_id
  can serve multiple protocols)
- Unify truncation notice logic for all providers (no system-role notice
  in middle of messages, preventing Anthropic non-consecutive system
  message errors)
- Pass model_type from model_config through MessageCompressor to
  TokenCounter for accurate provider detection
- Remove duplicate provider constants from factory.py and token_counter.py

Co-authored-by: wenbo17 <wenbo17@staff.weibo.com>

* ix: claude的子代理模式不可用修复 (#862)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: claude的子代理模式不可用修复

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* feat(inbox): implement work queue and message forwarding system (#851)

* feat(inbox): implement work queue and message forwarding system

Add a comprehensive message forwarding and work queue system that enables
users to forward conversation messages to other users. Recipients can
manage received messages through work queues and optionally configure
AI agents for automatic processing.

Backend changes:
- Add QueueMessage and RecentContact database models
- Add WorkQueue CRD schema stored in kinds table
- Implement work queue services for CRUD operations
- Implement message forwarding service with queue selection
- Add WebSocket events for real-time notifications
- Add API endpoints for work queue management
- Add ForbiddenException to core exceptions

Frontend changes:
- Add inbox page with queue sidebar and message list
- Implement ForwardMessageDialog for recipient selection
- Add queue edit/create dialog with auto-processing config
- Add message detail dialog with content preview
- Add i18n translations (zh-CN and en)
- Register inbox route and navigation

* fix: resolve lint errors in inbox feature

- Remove unused imports (RefreshCw, Settings, Menu, Button, Input, Clock)
- Format Python files with Black

* fix: resolve TypeScript errors in inbox feature

- Add 'flow' and 'inbox' to TopNavigation activePage type
- Replace Badge variant 'outline' with 'info' (outline not supported)

* fix: address CodeRabbit review comments for work queue feature

- Fix status parameter shadowing fastapi status module in work_queue.py
- Fix NamespaceMember.namespace to use group_name column reference
- Replace func.json_set with Python-level update for PostgreSQL compatibility
- Fix refreshQueues dependency loop using functional state update
- Remove Chinese text from inbox page comment
- Add NaN validation for schedule interval input
- Replace clickable Badge with accessible button for recent contacts

---------

Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>

* Wegent/work queue and message forwarding (#863)

* feat(inbox): implement work queue and message forwarding system

Add a comprehensive message forwarding and work queue system that enables
users to forward conversation messages to other users. Recipients can
manage received messages through work queues and optionally configure
AI agents for automatic processing.

Backend changes:
- Add QueueMessage and RecentContact database models
- Add WorkQueue CRD schema stored in kinds table
- Implement work queue services for CRUD operations
- Implement message forwarding service with queue selection
- Add WebSocket events for real-time notifications
- Add API endpoints for work queue management
- Add ForbiddenException to core exceptions

Frontend changes:
- Add inbox page with queue sidebar and message list
- Implement ForwardMessageDialog for recipient selection
- Add queue edit/create dialog with auto-processing config
- Add message detail dialog with content preview
- Add i18n translations (zh-CN and en)
- Register inbox route and navigation

* fix: resolve lint errors in inbox feature

- Remove unused imports (RefreshCw, Settings, Menu, Button, Input, Clock)
- Format Python files with Black

* fix: resolve TypeScript errors in inbox feature

- Add 'flow' and 'inbox' to TopNavigation activePage type
- Replace Badge variant 'outline' with 'info' (outline not supported)

* fix: address CodeRabbit review comments for work queue feature

- Fix status parameter shadowing fastapi status module in work_queue.py
- Fix NamespaceMember.namespace to use group_name column reference
- Replace func.json_set with Python-level update for PostgreSQL compatibility
- Fix refreshQueues dependency loop using functional state update
- Remove Chinese text from inbox page comment
- Add NaN validation for schedule interval input
- Replace clickable Badge with accessible button for recent contacts

* feat: update work queue

---------

Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>

* fix: fix column default value

* fix: fix forward message click

* feat: remove unused area

* feat: add inbox default message

* fix: 修复脏数据导致的订阅任务无法执行的问题 (#865)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复脏数据导致的订阅任务无法执行的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix(browser): harden browser-tool linux recovery (#867)

Co-authored-by: sifang <sifang@staff.weibo.com>

* fix: 修复无法使用自定义镜像以及自定义镜像无法校验的问题 (#866)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复无法使用自定义镜像

* fix: 修复无法使用自定义镜像以及自定义镜像无法校验的问题

* fix: 修复可能的bug

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fix welcome timezone (#868)

* docs: update inbox docs (#869)

* fix: 修复单测 (#870)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复单测

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fix page error (#871)

* fix: data-roleid="api-key-create-button" not in html attr (#873)

* fix: 修复停止按钮偶发变成发送的问题 & 修复失败的消息会丢失的问题 (#876)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复停止按钮偶发变成发送的问题

* fix: 修复失败的消息会丢失的问题

* fix: 修复类型报错

* fix: 修复前端复制按钮内容错误的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: 修复“skill not found” (#877)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 技能错乱

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* chore(executor): upgrade claude-agent-sdk from 0.1.31 to 0.1.51 (#880)

Co-authored-by: sifang <sifang@staff.weibo.com>

* feat(frontend): add HTML attachment preview and share link generation (#872)

- Add HtmlPreview component for previewing HTML files with iframe and source view modes
- Update FilePreview to support HTML file type detection and rendering
- Add createAttachmentShareLink API for generating public share links
- Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
- Add Web Page type label and icon for HTML attachments in AttachmentCard
- Fix preview page height to fill viewport using h-screen and h-full
- Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments

* fix(knowledge): harden document indexing state (#875)

* fix(knowledge): harden document indexing state

* fix(knowledge): harden indexing state transitions

* fix(knowledge): tighten reindex review followups

* fix(rag): KNOWLEDGE_INDEX_LOCK_MAX_RETRIES=1

* fix(knowledge): store index status without db enum

* fix(alembic): correct migration revision metadata

* docs: update contributors list (zh) (#879)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* docs: update contributors list (#878)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat: 支持skill认证 (#882)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: add skill http identity design and plan

* feat(skill): add http identity token flow

* feat: 修改样式

* feat: 修改样式

* feat: 移除计划

* feat: 调整为带有认证的接口

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>
# Conflicts:
#	executor_manager/services/sandbox/manager.py

* Feature/knowledge document read (#888)

* docs(backend): add knowledge document read spec and plan

* feat(backend): add orchestrated knowledge document reads

* fix(backend): tighten knowledge document read handling

* refactor(web-scraper): add configuration for specific sites that require special handling (#884)

* refactor(web-scraper): extract site-specific config to scraper_config.py

- Add scraper_config.py for site-specific crawling configurations

* fix(web_scraper): load WEB_SCRAPER_SITE_CONFIG from Pydantic settings instead of os.environ

- Add WEB_SCRAPER_SITE_CONFIG to Settings class in config.py
- Refactor scraper_config.py to use settings.WEB_SCRAPER_SITE_CONFIG with proper logging
- Replace module-level config loading with cached get_site_config() function
- Update scraper_service.py to use new get_site_config() function
- Add detailed logging for site config matching and application
- Fix remove_overlay_elements duplicate key in CrawlerRunConfig

This ensures the configuration is properly loaded from .env file via Pydantic settings."

* refactor(backend): improve scraper_config security and type safety

- Remove sensitive config value from INFO level logging
- Add return type annotations to _load_site_config and get_site_config
- Replace broad Exception catch with specific exception types (AttributeError, KeyError, TypeError, ValueError)
- Import Dict and Any from typing module

* refactor(backend): replace auto-cleanup with archive expiration check

Remove automatic MinIO archive cleanup (Celery Beat task) and instead
block task execution when workspace archive has expired (>30 days).
Users will receive a clear error message and must create a new task.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 添加恢复行为测试和脚本路径修正

* fix: 使用 timezone.utc 替换 UTC 以确保时区正确

* test(backend): fix workspace recovery test patches

---------

Co-authored-by: yunpeng7 <yunpeng7@staff.weibo.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: axb <qindi@staff.weibo.com>
Co-authored-by: moqimoqidea <39821951+moqimoqidea@users.noreply.github.com>
Co-authored-by: MicroLee <yudexiaozhou@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>
Co-authored-by: icycrystal4 <icycrystal4@gmail.com>
Co-authored-by: sifang <sifang@staff.weibo.com>
Co-authored-by: Yanhe <yanhe1@staff.weibo.com>
Co-authored-by: parabala <chenyong1@staff.weibo.com>
Co-authored-by: wenbo17 <wenbo17@staff.weibo.com>
Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>
Co-authored-by: 李枨煊 <chengxuan@staff.weibo.com>
Co-authored-by: johnny0120 <jiangyang7@staff.weibo.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
qdaxb added a commit that referenced this pull request Apr 3, 2026
* feat: add workspace archive and recovery for Pod deletion

Implement Pod recovery feature that allows users to continue conversations
after executor Pods are deleted:

- Add ArchiveInfo schema to store archive metadata in Task.status.archive
- Add workspace_archive service for MinIO presigned URL generation
- Add archive/restore endpoints in executor and executor_manager
- Add ExecutorRecoveryService for Pod recreation with workspace restoration
- Add skip_git_clone support to prevent git clone conflicts during restore
- Add Celery Beat scheduled task for expired archive cleanup (30-day retention)
- Integrate archive workflow in executor_job before Pod deletion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(backend): add missing minio dependency

Add minio package to backend dependencies for workspace archive storage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(admin): add created_at field to device monitor list (#846)

- Add created_at field to AdminDeviceInfo schema in backend
- Add created_at to AdminDeviceInfo TypeScript interface
- Display creation time in device list UI
- Add i18n translations for created_at column (zh-CN and en)

Co-authored-by: yunpeng7 <yunpeng7@staff.weibo.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* perf: use async heartbeat (#849)

* fix(chat_shell): prevent orphaned tool call IDs in group chat history (#852)

* fix(chat_shell): prevent tool call group splitting in group chat history truncation

_truncate_history blindly sliced the flat message list, which could
split assistant+tool_calls and their tool response messages across the
truncation boundary. This produced orphaned function_call_output items
without matching function_call entries in the OpenAI Responses API
format, causing 'No tool call found for function call output' errors.

Add _group_messages() to treat assistant+tool_calls+tool responses as
atomic units, and modify _truncate_history() to truncate by groups
instead of individual messages.

* fix(chat_shell): ensure tool call group integrity across all compression paths

Extract group_tool_call_messages() into shared messages/utils.py and
apply atomic group handling to:
- HistoryTruncationStrategy.compress(): adjust first/last boundaries
  to avoid splitting tool call groups, add post-removal orphan cleanup
- _force_compression_to_target(): Step 2 removes middle groups instead
  of individual messages, Step 5 keeps first/last groups
- Refactor loader.py to import shared utility instead of inline helper

All 95 related tests pass. Comprehensive audit confirms no other code
paths in chat_shell break tool call group integrity.

* test(chat_shell): add tool-call group truncation regressions

Add regression coverage for preserving assistant tool calls with their matching tool results across group-chat history truncation, history compression boundaries, and forced compression fallback paths.

* fix: 在里面智能体的创建人和机器人的创建人不是一个人时会报错 (#853)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 在里面智能体的创建人和机器人的创建人不是一个人时会报错

* fix: 迁移其他的sql到kindReader

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fail fast on invalid tool call IDs across chat protocols (#854)

* fix: 订阅任务执行间隔调整 & trace http接口的匹配规则调整 (#855)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: telemetry的匹配规则调整

* fix: 订阅任务执行间隔调整

* fix: 调整逻辑

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: sanitize PyInstaller LD_LIBRARY_PATH for child processes (#859)

PyInstaller's bootloader prepends its extraction directory (/tmp/_MEIxxxxxx)
to LD_LIBRARY_PATH, causing child processes (Claude Code, shell commands)
to load wrong shared libraries (e.g., libtinfo.so.6).

Extract `sanitize_ld_library_path()` into platform_compat and apply it in
both local_mode_strategy (Claude SDK subprocess) and extension_handler
(extension script subprocess) to restore the original library path.

See: https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html

Co-authored-by: sifang <sifang@staff.weibo.com>

* refactor(rag): move internal retrieve routing into backend (#847)

* refactor(rag): move internal retrieve routing into backend

* refactor(rag): move retrieve persistence into backend

* refactor(rag): move kb head persistence into backend

* fix(rag): tighten backend retrieve routing

* refactor(rag): simplify token estimation fallback

* refactor(rag): batch route token estimation

* fix(rag): token estimator decimal

* fix(rag): harden kb read persistence

* frontend: show 'indexing' status for documents pending indexing (#858)

* fix(frontend): show 'indexing' status for documents pending indexing

When a document is uploaded, is_active is false while indexing is in progress.
Previously, this showed 'index unavailable' which was misleading.

Now documents with status='enabled' but is_active=false will show 'indexing'
instead of 'index unavailable', matching the existing translation key.

Changes:
- Updated compact mode status indicator (line 247)
- Updated normal mode status badge (line 461)

Fixes: knowledge base document shows 'index unavailable' during indexing

* fix(i18n): update Developer role description in Chinese locale

* fix: 修复订阅器中脏数据导致的无法展示的问题 (#860)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复订阅器中脏数据导致的无法展示的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* feat(backend): add knowledge base RAG support to /v1/responses API (#844)

* feat(backend): add knowledge base RAG support to /v1/responses API

Implement knowledge base Q&A functionality for the OpenAI-compatible
/v1/responses endpoint with the following changes:

Schema Changes:
- Add knowledge_base_names field to WegentTool schema
- Support namespace#name format for KB specification

New Services:
- KnowledgeBaseNameResolver: resolves KB display names to IDs with
  permission checking via TaskKnowledgeBaseService
- KnowledgeBaseContextCreator: creates SubtaskContext records for
  API-specified KBs to integrate with existing RAG pipeline

Integration:
- Extend parse_wegent_tools() to handle knowledge_base tool type
- Add parse_knowledge_base_name() for namespace#name parsing
- Modify build_execution_request() to accept knowledge_base_names
- Add _create_kb_contexts_from_api_request() for KB context injection
- Update openapi_responses.py endpoints to pass KB names parameter

Testing:
- Add comprehensive unit tests for KB name resolution
- Add tests for KB context creation
- Add tests for helper functions with KB support
- All 32 new tests passing

The implementation reuses the existing RAG flow through
prepare_contexts_for_chat() and KnowledgeBaseTool, ensuring
consistency with the web UI behavior.

* refactor(kb_resolver): optimize database query complexity from O(n) to O(m)

- Add batch_get_knowledge_base_by_names() to TaskKnowledgeBaseService for efficient batch queries
- Extract _check_kb_access_permission() for unified permission checking logic
- Update kb_resolver to use service methods instead of direct queries
- Reduce DB queries from O(n) to O(m) where m is unique namespace count
- Update tests to mock service methods instead of internal functions

All 15 tests pass.

* refactor(backend): optimize KB permission check in kb_resolver

Simplify permission checking by using get_all_knowledge_bases_grouped()
which already returns all KBs accessible to the user.

Changes:
- Replace _batch_query_kbs() and _check_kb_access_permission() with
  _get_accessible_kb_lookup() that builds a lookup dict from grouped results
- Remove unused resolve_single() method and batch_get_knowledge_base_by_names()
- Update tests to mock KnowledgeService.get_all_knowledge_bases_grouped()

This reduces database queries and simplifies the permission check logic.

* feat(backend): integrate knowledge base context creation into unified AI trigger

- Add knowledge_base_names parameter to build_execution_request for OpenAPI v1/responses
- Create _create_kb_contexts_from_api_request helper to process KB names from API requests
- Integrate KB context processing into unified trigger flow
- Add KnowledgeBaseContextCreator class for creating SubtaskContext records
- Add comprehensive unit tests for KnowledgeBaseContextCreator

This enables RAG functionality for OpenAPI v1/responses endpoint by creating
SubtaskContext records that are processed by the existing context pipeline.

* fix(chat): propagate HTTPException from KnowledgeBaseNameResolver in unified trigger

- Add HTTPException import from fastapi
- Catch and re-raise HTTPException explicitly in _create_kb_contexts_from_api_request
- Keep generic exception handling for non-HTTP exceptions to preserve best-effort behavior
- This ensures 403/404 errors from KB resolution are properly propagated to callers
  instead of being swallowed and logged as warnings

* fix(chat_shell): use model_type for provider detection instead of model_id (#861)

- Extract shared detect_provider() into models/providers.py with
  PROVIDER_ALIASES as single source of truth
- model_type (env.model) is authoritative; raise ValueError on unknown
- Remove _PROVIDER_ID_PATTERNS model_id prefix fallback (same model_id
  can serve multiple protocols)
- Unify truncation notice logic for all providers (no system-role notice
  in middle of messages, preventing Anthropic non-consecutive system
  message errors)
- Pass model_type from model_config through MessageCompressor to
  TokenCounter for accurate provider detection
- Remove duplicate provider constants from factory.py and token_counter.py

Co-authored-by: wenbo17 <wenbo17@staff.weibo.com>

* ix: claude的子代理模式不可用修复 (#862)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: claude的子代理模式不可用修复

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* feat(inbox): implement work queue and message forwarding system (#851)

* feat(inbox): implement work queue and message forwarding system

Add a comprehensive message forwarding and work queue system that enables
users to forward conversation messages to other users. Recipients can
manage received messages through work queues and optionally configure
AI agents for automatic processing.

Backend changes:
- Add QueueMessage and RecentContact database models
- Add WorkQueue CRD schema stored in kinds table
- Implement work queue services for CRUD operations
- Implement message forwarding service with queue selection
- Add WebSocket events for real-time notifications
- Add API endpoints for work queue management
- Add ForbiddenException to core exceptions

Frontend changes:
- Add inbox page with queue sidebar and message list
- Implement ForwardMessageDialog for recipient selection
- Add queue edit/create dialog with auto-processing config
- Add message detail dialog with content preview
- Add i18n translations (zh-CN and en)
- Register inbox route and navigation

* fix: resolve lint errors in inbox feature

- Remove unused imports (RefreshCw, Settings, Menu, Button, Input, Clock)
- Format Python files with Black

* fix: resolve TypeScript errors in inbox feature

- Add 'flow' and 'inbox' to TopNavigation activePage type
- Replace Badge variant 'outline' with 'info' (outline not supported)

* fix: address CodeRabbit review comments for work queue feature

- Fix status parameter shadowing fastapi status module in work_queue.py
- Fix NamespaceMember.namespace to use group_name column reference
- Replace func.json_set with Python-level update for PostgreSQL compatibility
- Fix refreshQueues dependency loop using functional state update
- Remove Chinese text from inbox page comment
- Add NaN validation for schedule interval input
- Replace clickable Badge with accessible button for recent contacts

---------

Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>

* Wegent/work queue and message forwarding (#863)

* feat(inbox): implement work queue and message forwarding system

Add a comprehensive message forwarding and work queue system that enables
users to forward conversation messages to other users. Recipients can
manage received messages through work queues and optionally configure
AI agents for automatic processing.

Backend changes:
- Add QueueMessage and RecentContact database models
- Add WorkQueue CRD schema stored in kinds table
- Implement work queue services for CRUD operations
- Implement message forwarding service with queue selection
- Add WebSocket events for real-time notifications
- Add API endpoints for work queue management
- Add ForbiddenException to core exceptions

Frontend changes:
- Add inbox page with queue sidebar and message list
- Implement ForwardMessageDialog for recipient selection
- Add queue edit/create dialog with auto-processing config
- Add message detail dialog with content preview
- Add i18n translations (zh-CN and en)
- Register inbox route and navigation

* fix: resolve lint errors in inbox feature

- Remove unused imports (RefreshCw, Settings, Menu, Button, Input, Clock)
- Format Python files with Black

* fix: resolve TypeScript errors in inbox feature

- Add 'flow' and 'inbox' to TopNavigation activePage type
- Replace Badge variant 'outline' with 'info' (outline not supported)

* fix: address CodeRabbit review comments for work queue feature

- Fix status parameter shadowing fastapi status module in work_queue.py
- Fix NamespaceMember.namespace to use group_name column reference
- Replace func.json_set with Python-level update for PostgreSQL compatibility
- Fix refreshQueues dependency loop using functional state update
- Remove Chinese text from inbox page comment
- Add NaN validation for schedule interval input
- Replace clickable Badge with accessible button for recent contacts

* feat: update work queue

---------

Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>

* fix: fix column default value

* fix: fix forward message click

* feat: remove unused area

* feat: add inbox default message

* fix: 修复脏数据导致的订阅任务无法执行的问题 (#865)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复脏数据导致的订阅任务无法执行的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix(browser): harden browser-tool linux recovery (#867)

Co-authored-by: sifang <sifang@staff.weibo.com>

* fix: 修复无法使用自定义镜像以及自定义镜像无法校验的问题 (#866)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复无法使用自定义镜像

* fix: 修复无法使用自定义镜像以及自定义镜像无法校验的问题

* fix: 修复可能的bug

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fix welcome timezone (#868)

* docs: update inbox docs (#869)

* fix: 修复单测 (#870)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复单测

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: fix page error (#871)

* fix: data-roleid="api-key-create-button" not in html attr (#873)

* fix: 修复停止按钮偶发变成发送的问题 & 修复失败的消息会丢失的问题 (#876)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 修复停止按钮偶发变成发送的问题

* fix: 修复失败的消息会丢失的问题

* fix: 修复类型报错

* fix: 修复前端复制按钮内容错误的问题

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* fix: 修复“skill not found” (#877)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* fix: 技能错乱

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>

* chore(executor): upgrade claude-agent-sdk from 0.1.31 to 0.1.51 (#880)

Co-authored-by: sifang <sifang@staff.weibo.com>

* feat(frontend): add HTML attachment preview and share link generation (#872)

- Add HtmlPreview component for previewing HTML files with iframe and source view modes
- Update FilePreview to support HTML file type detection and rendering
- Add createAttachmentShareLink API for generating public share links
- Add share button to FilePreviewDialog and FilePreviewPage (visible for owners only)
- Add Web Page type label and icon for HTML attachments in AttachmentCard
- Fix preview page height to fill viewport using h-screen and h-full
- Update AttachmentPreview to support HTML file preview button

Fixes share link browsing and download for table-type attachments

* fix(knowledge): harden document indexing state (#875)

* fix(knowledge): harden document indexing state

* fix(knowledge): harden indexing state transitions

* fix(knowledge): tighten reindex review followups

* fix(rag): KNOWLEDGE_INDEX_LOCK_MAX_RETRIES=1

* fix(knowledge): store index status without db enum

* fix(alembic): correct migration revision metadata

* docs: update contributors list (zh) (#879)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* docs: update contributors list (#878)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat: 支持skill认证 (#882)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: update contributors list

* docs: update contributors list (zh)

* docs: add skill http identity design and plan

* feat(skill): add http identity token flow

* feat: 修改样式

* feat: 修改样式

* feat: 移除计划

* feat: 调整为带有认证的接口

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>
# Conflicts:
#	executor_manager/services/sandbox/manager.py

* Feature/knowledge document read (#888)

* docs(backend): add knowledge document read spec and plan

* feat(backend): add orchestrated knowledge document reads

* fix(backend): tighten knowledge document read handling

* refactor(web-scraper): add configuration for specific sites that require special handling (#884)

* refactor(web-scraper): extract site-specific config to scraper_config.py

- Add scraper_config.py for site-specific crawling configurations

* fix(web_scraper): load WEB_SCRAPER_SITE_CONFIG from Pydantic settings instead of os.environ

- Add WEB_SCRAPER_SITE_CONFIG to Settings class in config.py
- Refactor scraper_config.py to use settings.WEB_SCRAPER_SITE_CONFIG with proper logging
- Replace module-level config loading with cached get_site_config() function
- Update scraper_service.py to use new get_site_config() function
- Add detailed logging for site config matching and application
- Fix remove_overlay_elements duplicate key in CrawlerRunConfig

This ensures the configuration is properly loaded from .env file via Pydantic settings."

* refactor(backend): improve scraper_config security and type safety

- Remove sensitive config value from INFO level logging
- Add return type annotations to _load_site_config and get_site_config
- Replace broad Exception catch with specific exception types (AttributeError, KeyError, TypeError, ValueError)
- Import Dict and Any from typing module

* refactor(backend): replace auto-cleanup with archive expiration check

Remove automatic MinIO archive cleanup (Celery Beat task) and instead
block task execution when workspace archive has expired (>30 days).
Users will receive a clear error message and must create a new task.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 添加恢复行为测试和脚本路径修正

* fix: 使用 timezone.utc 替换 UTC 以确保时区正确

* test(backend): fix workspace recovery test patches

* feat: 添加工作区归档配置测试与默认值

* feat: 添加工作区归档时区配置及测试

---------

Co-authored-by: yunpeng7 <yunpeng7@staff.weibo.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: axb <qindi@staff.weibo.com>
Co-authored-by: moqimoqidea <39821951+moqimoqidea@users.noreply.github.com>
Co-authored-by: MicroLee <yudexiaozhou@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: hongyu9 <hongyu9@staff.weibo.com>
Co-authored-by: icycrystal4 <icycrystal4@gmail.com>
Co-authored-by: sifang <sifang@staff.weibo.com>
Co-authored-by: Yanhe <yanhe1@staff.weibo.com>
Co-authored-by: parabala <chenyong1@staff.weibo.com>
Co-authored-by: wenbo17 <wenbo17@staff.weibo.com>
Co-authored-by: qdaxb <4157870+qdaxb@users.noreply.github.com>
Co-authored-by: 李枨煊 <chengxuan@staff.weibo.com>
Co-authored-by: johnny0120 <jiangyang7@staff.weibo.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
cocowh pushed a commit to cocowh/Wegent that referenced this pull request May 11, 2026
feat(backend): migrate KB task refs from name-based to ID-based storage (wecode-ai#872)

See merge request weibo_rd/common/wecode/wegent!1068
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants