Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ When modifying server APIs:
### Browser Extension Patterns
- **Content Scripts**: Message passing between content script and background worker
- **Auto-save**: Special handling for Twitter timeline interception (`tweet_interceptor.js`)
- **Multi-browser**: Separate Firefox builds via `BROWSER=firefox` env var

## Critical Integration Points

Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
## Build, Test, and Development Commands
- Server: `cd app/server` and use `./start-dev.sh` (primary dev mode, supports `--task` to enable connectors and `--sql` to show SQL); `./mvnw clean verify` builds everything; direct maven run: `./mvnw spring-boot:run -pl huntly-server -am` serves `localhost:8080`.
- Web client: `cd app/client && yarn install && yarn start`; use `yarn build` for production assets and `yarn test` for the Jest suite.
- Browser extension: `cd app/extension && yarn install && yarn dev`; guard releases with `yarn build` and `yarn test`. Use `yarn build:firefox` for Firefox-specific builds.
- Browser extension: `cd app/extension && yarn install && yarn dev`; guard releases with `yarn build` and `yarn test`.
- Desktop app (Tauri): `cd app/tauri && yarn install && yarn tauri dev`; `yarn build` compiles frontend and `yarn tauri build` bundles the desktop app.
- Containers: `docker-compose up -d` runs the published image; `docker build -t huntly-local -f Dockerfile .` produces a workspace-aware image.

Expand All @@ -26,3 +26,6 @@ Avoid committing SQLite artifacts in `app/server/huntly-server/db.sqlite*`; pers
When updating the project's README, ensure all language versions are updated consistently:
- `README.md` (English)
- `README.zh.md` (Chinese)

## UI Language Guidelines
All user interface text must be written in English. This applies to button labels, menu items, tooltips, error messages, notifications, form labels, and any user-facing strings across all clients (browser extension, web client, desktop app).
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ yarn watch
# Production build
yarn build

# Firefox-specific builds
yarn watch:firefox
yarn build:firefox

# Run tests
yarn test

Expand Down Expand Up @@ -192,4 +188,11 @@ When updating the project's README, ensure all language versions are updated con
- **Repository Pattern**: JPA repositories with custom specifications in `huntly-jpa`
- **DTO Mapping**: MapStruct for entity-DTO conversion
- **Event-Driven**: Application events for decoupled processing
- **Streaming**: Server-Sent Events for real-time AI content processing
- **Streaming**: Server-Sent Events for real-time AI content processing

## UI Language Guidelines
All user interface text must be written in English. This applies to:
- Button labels, menu items, and tooltips
- Error messages and notifications
- Form labels and placeholder text
- Any user-facing strings in the browser extension, web client, and desktop app
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Self-hosted information hub with a powerful browser extension that captures, pro
| Feature | Description |
|---------|-------------|
| 🤖 **AI Content Processing** | Leverage AI for summarization, translation, and intelligent content analysis with custom shortcuts |
| 🔌 **MCP Server Integration** | Built-in Model Context Protocol (MCP) server enabling AI assistants (Claude, Cursor, etc.) to access your knowledge base, search content, and retrieve RSS feeds, tweets, GitHub stars, and highlights |
| 🔌 **MCP & Agent Skills** | MCP server + Agent Skills for AI assistants to search your knowledge base, RSS feeds, tweets, and highlights. Install via `npx skills add lcomplete/huntly` |
| 📚 **Web Archiving** | Automatically save and archive web pages with content extraction using Defuddle and Mozilla Readability |
| 📡 **RSS Feed Management** | Centralize all your RSS feeds with intelligent categorization, OPML import/export, and full-text search |
| 🔍 **Powerful Full-Text Search** | Apache Lucene with IK Analyzer for Chinese text tokenization, boolean operators, and fuzzy search |
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
| 功能 | 描述 |
|---------|-------------|
| 🤖 **AI 内容处理** | 利用 AI 进行摘要、翻译和智能内容分析,支持自定义快捷指令 |
| 🔌 **MCP 服务器集成** | 内置 Model Context Protocol (MCP) 服务器,让 AI 助手(Claude、Cursor 等)可以访问您的知识库、搜索内容、获取 RSS 订阅、推文、GitHub stars 和高亮标注 |
| 🔌 **MCP & Agent Skills** | MCP 服务器 + Agent Skills,让 AI 助手可搜索您的知识库、RSS 订阅、推文和高亮标注。通过 `npx skills add lcomplete/huntly` 安装 |
| 📚 **网页归档** | 使用 Defuddle 和 Mozilla Readability 自动保存和归档网页,提取正文内容 |
| 📡 **RSS 订阅管理** | 集中管理所有 RSS 订阅,支持智能分类、OPML 导入/导出和全文搜索 |
| 🔍 **强大的全文搜索** | Apache Lucene 搜索引擎,IK 分词器支持中文分词,布尔运算符和模糊搜索 |
Expand Down
24 changes: 11 additions & 13 deletions app/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"description": "Huntly - Automatic saving browsed contents",
"main": "index.js",
"scripts": {
"dev": "webpack --config webpack/webpack.dev.js --watch",
"watch": "webpack --config webpack/webpack.dev.js --watch",
"watch:firefox": "cross-env BROWSER=firefox npm run watch",
"build": "webpack --config webpack/webpack.prod.js",
"build:firefox": "cross-env BROWSER=firefox npm run build",
"clean": "rimraf dist",
"dev": "rspack build --watch --mode development",
"watch": "rspack build --watch --mode development",
"watch:firefox": "cross-env BROWSER=firefox rspack build --watch --mode development",
"build": "rspack build --mode production",
"clean": "rimraf dist dist_firefox",
"test": "npx jest",
"typecheck": "tsc --noEmit",
"style": "prettier --write \"src/**/*.{ts,tsx}\""
},
"author": "",
Expand All @@ -32,9 +32,10 @@
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.14",
"@types/turndown": "^5.0.6",
"ai": "^4.3.16",
"ai": "^6.0.127",
"defuddle": "^0.6.6",
"formik": "^2.2.9",
"html2canvas": "^1.4.1",
"ollama-ai-provider": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -46,13 +47,14 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@rspack/cli": "^1.7.8",
"@rspack/core": "^1.7.8",
"@types/chrome": "0.0.158",
"@types/jest": "^27.0.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-modal": "^3.16.0",
"autoprefixer": "^10.4.14",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"glob": "^7.1.6",
Expand All @@ -65,10 +67,6 @@
"style-loader": "^3.3.1",
"tailwindcss": "^3.3.2",
"ts-jest": "^27.0.5",
"ts-loader": "^8.0.0",
"typescript": "^4.4.3 ",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"webpack-merge": "^5.0.0"
"typescript": "^4.4.3 "
}
}
2 changes: 1 addition & 1 deletion app/extension/public/manifest-firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"<all_urls>"
],
"js": [
"js/vendor.js",
"js/content_script.js"
],
"run_at": "document_start"
Expand All @@ -43,6 +42,7 @@
"<all_urls>"
],
"js": [
"js/web_clipper_vendor.js",
"js/web_clipper.js"
],
"run_at": "document_end"
Expand Down
2 changes: 1 addition & 1 deletion app/extension/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"<all_urls>"
],
"js": [
"js/vendor.js",
"js/content_script.js"
],
"run_at": "document_start"
Expand All @@ -44,6 +43,7 @@
"<all_urls>"
],
"js": [
"js/web_clipper_vendor.js",
"js/web_clipper.js"
],
"run_at": "document_end"
Expand Down
159 changes: 159 additions & 0 deletions app/extension/rspack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const path = require("path");
const { rspack } = require("@rspack/core");

const rootDir = __dirname;
const srcDir = path.join(rootDir, "src");
const publicDir = path.join(rootDir, "public");

const isFirefoxBuild = process.env.BROWSER === "firefox";
const outputDir = path.resolve(rootDir, isFirefoxBuild ? "dist_firefox" : "dist");
const manifestSource = path.join(
publicDir,
isFirefoxBuild ? "manifest-firefox.json" : "manifest.json",
);
const extensionVersion = process.env.EXTENSION_VERSION;
const popupOptionEntries = new Set(["popup", "options"]);

module.exports = (_, argv = {}) => {
const mode = argv.mode || process.env.NODE_ENV || "production";
const isDevelopment = mode === "development";

return {
mode,
context: rootDir,
entry: {
popup: path.join(srcDir, "popup.tsx"),
options: path.join(srcDir, "options.tsx"),
background: path.join(srcDir, "background.ts"),
content_script: path.join(srcDir, "content_script.tsx"),
tweet_interceptor: path.join(srcDir, "tweet_interceptor.ts"),
web_clipper: path.join(srcDir, "web_clipper.tsx"),
},
output: {
path: path.join(outputDir, "js"),
filename: "[name].js",
chunkFilename: "[name].js",
clean: true,
},
devtool: isDevelopment ? "cheap-source-map" : false,
optimization: {
splitChunks: {
cacheGroups: {
popupOptionsVendor: {
name: "vendor",
test: /[\\/]node_modules[\\/]/,
chunks(chunk) {
return popupOptionEntries.has(chunk.name);
},
minChunks: 1,
enforce: true,
},
webClipperVendor: {
name: "web_clipper_vendor",
test: /[\\/]node_modules[\\/]/,
chunks(chunk) {
return chunk.name === "web_clipper";
},
minChunks: 1,
enforce: true,
},
},
},
},
module: {
rules: [
{
resourceQuery: /raw/,
type: "asset/source",
},
{
test: /\.module\.css$/i,
resourceQuery: { not: [/raw/] },
use: [
{ loader: "style-loader" },
{
loader: "css-loader",
options: {
modules: {
localIdentName: "huntly-ext-[local]-[hash:base64:5]",
},
},
},
{ loader: "postcss-loader" },
],
},
{
test: /\.css$/i,
exclude: /\.module\.css$/i,
resourceQuery: { not: [/raw/] },
use: [
{ loader: "style-loader" },
{ loader: "css-loader" },
{ loader: "postcss-loader" },
],
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
type: "javascript/auto",
use: {
loader: "builtin:swc-loader",
options: {
sourceMaps: isDevelopment,
jsc: {
parser: {
syntax: "typescript",
tsx: true,
},
target: "es2015",
transform: {
react: {
runtime: "classic",
development: isDevelopment,
refresh: false,
},
},
},
},
},
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".css"],
},
watchOptions: isDevelopment
? {
ignored: /node_modules/,
aggregateTimeout: 80,
followSymlinks: false,
}
: undefined,
plugins: [
new rspack.CopyRspackPlugin({
patterns: [
{
context: publicDir,
from: "**/*",
to: outputDir,
noErrorOnMissing: true,
globOptions: {
ignore: ["**/manifest*.json"],
},
},
{
from: manifestSource,
to: path.resolve(outputDir, "manifest.json"),
transform(content) {
const manifest = JSON.parse(content.toString());
if (extensionVersion) {
manifest.version = extensionVersion;
}
return JSON.stringify(manifest, null, 2);
},
},
],
}),
],
};
};
77 changes: 77 additions & 0 deletions app/extension/src/__tests__/exportUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/** @jest-environment jsdom */

import {
calculateExportScale,
sanitizeClonedMediaForExport,
} from "../utils/exportUtils";

function createDomRect(width: number, height: number): DOMRect {
return {
width,
height,
top: 0,
right: width,
bottom: height,
left: 0,
x: 0,
y: 0,
toJSON: () => ({}),
} as DOMRect;
}

describe("calculateExportScale", () => {
it("keeps the default scale for regular content sizes", () => {
expect(calculateExportScale(900, 1200)).toBe(2);
});

it("keeps long but still safe pages at 2x quality", () => {
expect(calculateExportScale(1200, 15000)).toBe(2);
});

it("reduces the scale for very large exports", () => {
expect(calculateExportScale(9000, 20000)).toBeLessThan(2);
});
});

describe("sanitizeClonedMediaForExport", () => {
it("replaces images that fail the canvas taint test and strips unsafe backgrounds", () => {
const original = document.createElement("div");

const externalImage = document.createElement("img");
externalImage.src = "https://cdn.example.com/image.png";
externalImage.alt = "External image";
externalImage.getBoundingClientRect = () => createDomRect(320, 180);

const localImage = document.createElement("img");
localImage.src = "/local-image.png";
localImage.getBoundingClientRect = () => createDomRect(120, 80);

const backgroundNode = document.createElement("div");
backgroundNode.style.backgroundImage =
'url("https://cdn.example.com/background.png")';

original.append(externalImage, localImage, backgroundNode);

const cloned = original.cloneNode(true) as HTMLDivElement;
sanitizeClonedMediaForExport(
original,
cloned,
"http://localhost",
"http://localhost/article"
);

// In jsdom (no canvas support), all images fail the taint test and
// get replaced with placeholders — this is the safe default.
const placeholders = cloned.querySelectorAll(
'[data-huntly-export-omitted="true"]'
);
expect(placeholders.length).toBe(2);
expect(cloned.textContent).toContain("External image");
expect(cloned.querySelectorAll("img")).toHaveLength(0);

// Background images from cross-origin URLs are stripped
expect(
(cloned.lastElementChild as HTMLDivElement).style.backgroundImage
).toBe("none");
});
});
Loading
Loading