Skip to content
Draft
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 更新日志

## Unreleased

### 修复

- 修复从 ChatGPT 选中表格内容时,保存到 Obsidian 后表格被压平成单段文本的问题。现在会保留为 Markdown 表格。
- 修复点击 `AI 解释` 时误把最近问题一并写入 Obsidian 的问题。现在 `AI 解释` 只保存选中内容,`问答` 才会保存问题和回答。
- 修复扩展重新加载后,页面上旧浮动按钮可能触发 `Extension context invalidated` 的问题。新 content script 会接管旧按钮,并在旧上下文失效时给出刷新页面提示。
- 修复连续两次从同一条 ChatGPT 回答中保存 `AI 解释` 时重复生成标题的问题。现在同一回答的连续捕获会追加到同一个 `GPTNote AI 解释` 块下。

### 变更

- 为浏览器扩展拆分了选区 DOM 转 Markdown 和捕获 payload 构造逻辑,方便测试和后续维护。
- 捕获 payload 新增 `sourceMessageId` 字段,用于判断多次捕获是否来自同一条 ChatGPT 回答。

### 验证

- 已通过 `pnpm verify`,覆盖 build、typecheck 和测试。
2 changes: 1 addition & 1 deletion apps/browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"build": "node esbuild.config.mjs",
"dev": "node esbuild.config.mjs --watch",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
Expand All @@ -15,4 +16,3 @@
"@types/chrome": "*"
}
}

57 changes: 57 additions & 0 deletions apps/browser-extension/src/capture-payload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { buildSelectionCapturePayload } from "./capture-payload";

const context = {
pageUrl: "https://chatgpt.com/c/test",
pageTitle: "ChatGPT",
capturedAt: "2026-07-01T00:00:00.000Z",
};

describe("buildSelectionCapturePayload", () => {
it("sends only the selected content for AI explanation captures", () => {
expect(buildSelectionCapturePayload("llm-explanation", "selected answer", "nearest question", context)).toEqual({
type: "llm-explanation",
pageUrl: context.pageUrl,
pageTitle: context.pageTitle,
capturedAt: context.capturedAt,
answer: "selected answer",
});
});

it("sends the prompt only for Q&A captures", () => {
expect(buildSelectionCapturePayload("qa-pair", "selected answer", "nearest question", context)).toEqual({
type: "qa-pair",
pageUrl: context.pageUrl,
pageTitle: context.pageTitle,
capturedAt: context.capturedAt,
prompt: "nearest question",
answer: "selected answer",
});
});

it("sends quote captures as source text only", () => {
expect(buildSelectionCapturePayload("quote", "selected source", "nearest question", context)).toEqual({
type: "quote",
pageUrl: context.pageUrl,
pageTitle: context.pageTitle,
capturedAt: context.capturedAt,
sourceText: "selected source",
});
});

it("includes a source message id when the selected content belongs to one assistant answer", () => {
expect(
buildSelectionCapturePayload("llm-explanation", "selected answer", undefined, {
...context,
sourceMessageId: "https://chatgpt.com/c/test:assistant:0",
}),
).toEqual({
type: "llm-explanation",
pageUrl: context.pageUrl,
pageTitle: context.pageTitle,
capturedAt: context.capturedAt,
sourceMessageId: "https://chatgpt.com/c/test:assistant:0",
answer: "selected answer",
});
});
});
51 changes: 51 additions & 0 deletions apps/browser-extension/src/capture-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { CapturePayloadInput, CaptureType } from "@gptnote/shared";

type CapturePageContext = {
pageUrl: string;
pageTitle: string;
sourceMessageId?: string;
capturedAt?: string;
};

export function buildSelectionCapturePayload(
type: CaptureType,
selection: string,
prompt: string | undefined,
context: CapturePageContext,
): CapturePayloadInput {
const base: CapturePayloadInput = {
type,
pageUrl: context.pageUrl,
pageTitle: context.pageTitle,
capturedAt: context.capturedAt ?? new Date().toISOString(),
};

if (context.sourceMessageId) {
base.sourceMessageId = context.sourceMessageId;
}

if (type === "quote") {
return {
...base,
sourceText: selection,
};
}

if (type === "llm-explanation") {
return {
...base,
answer: selection,
};
}

const payload: CapturePayloadInput = {
...base,
answer: selection,
};

if (prompt) {
payload.prompt = prompt;
}

return payload;
}
Loading