diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index e87db404..1fd51994 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -257,6 +257,12 @@ AI 打标签的建议在应用或放弃前一直存着,刷新或重开分拣
- 内置播放框的 UP 标签行只给关注数据里有的 UP:在关注的可以加标签;已取消关注的只显示原来的标签、没有「+ 标签」;从没关注过的整行不显示。为什么:UP 标签挂在关注列表的 UP 上,没关注的人没有地方挂;另存一份「打了标签但没关注的 UP」会多出一套看不见、不跟着刷新的数据。视频投稿同理反过来:那些视频不在收藏夹,只能给 UP 打标签,不能给视频打。
- B站 自动连播到下一个视频时,两种模式的内置播放框都跟着它:粉色卡片、标签行和 T 都换到新视频。关注里认不出新视频的 UP(不在视频投稿、也不在 UP 卡片的最近投稿里)时,UP 标签行不显示,T 不做事。为什么:留在前一个视频上,T 会给前一个 UP 打标签。
+### 专注模式
+
+- 右上角的图标按钮是 36×36 的正圆,按钮组的右边缘和字幕栏的右边缘对齐。
+- 字幕行的时间戳比正文轻:`--mdg-faint`、常规字重、等宽数字。
+- 设置面板里的控件都是 28px 高;「跟随」「章节」是勾选框加文字,外面不套框。
+
### B站 页面上的标记
#### 定下来的决定(改之前先问用户)
@@ -359,6 +365,9 @@ B站 页面上的 AI 判断照上表:值得留绿、可清理红、拿不准
- 设置页各节按这个顺序排:读字幕、笔记格式、观看进度、AI 平台、分拣台、MCP、Obsidian、数据与存储、高级。用到 AI 的三节(AI 平台、分拣台、MCP)挨在一起,和 AI 无关的观看进度排在它们前面;MCP 排在分拣台后面,因为它的开关管分拣台和收藏。为什么:按和 AI 有没有关系分组,找 AI 相关的设置不用跳着看(2026-11-10 用户定)。
- 设置页「AI 平台」的「视频」「网页」是页签,照 B站 空间页的页签:16px,当前的加粗、下面 3px 粉线,旁边小灰字写用在哪(「B站 · YouTube」「其他网站」),一次只显示一组;左右方向键切换。每次打开设置页在「视频」。为什么:两组的项同名,摆在一起分不清在改哪一组(2026-10-22 用户定)。
+- 设置页的输入框、下拉框和同一行里的按钮都是 32px 高(`--control-h`)。左侧目录的当前项照「颜色」表:灰底、左边一道粉线、字是 `--text`。
+- 设置页 Obsidian 的「测试连接」在 Key 输入框右边,结果写在「Local REST API Key」名字下面(成功绿字、失败红字),没测过时不占位置。2026-11-14 用户定:填完 Key 手就在那里,也和 Obsidian 插件自己「左名字右输入框」的设置页对得上。
+- 弹窗里能滚动的框(字幕预览)有边框,不能滚动的(「详情」)只有底色。2026-11-14 用户定:边框表示这里可以滚动。
#### 定下来的决定(改之前先问用户)
@@ -374,12 +383,15 @@ B站 页面上的 AI 判断照上表:值得留绿、可清理红、拿不准
- 1024 宽时,第 2 行的搜索框和按钮换到页签下面一行。720 宽时没有左栏:左栏变成顶栏的下拉框(选项带数量),⚙ 放到第 2 行最后。
- 头部收起:第 2 行最前面有一个按钮,展开时是 ▴「收起头部」,收起时是 ▾「展开头部」,只有它收起和展开头部。收起后是一条细栏,只剩「名字 · 数量」、第 2 行的页签和右边的搜索 / 刷新 / 导出,没有封面,细栏自己是一张圆角白卡,上面留的空和展开时一样。滚动、回到顶上、焦点进到头部都不改它;切换收藏夹、页签和模式也不改。两种模式各记各的,重开分拣台后照旧;切到另一种模式时,它的收起状态当场生效,和重开分拣台一样不播动画。列表滚动后头部下沿出现阴影(展开时在第 4 行下面,收起时在细栏下面),滚回顶上就消失,说明卡片在头部底下;阴影不改收起状态。列表从头部底下穿过,收起和展开只是平移和淡出,卡片不跳;内置播放框的上沿跟着头部的下沿走。收起和展开用 0.15 秒,系统设了减少动态效果就不动画。760 及以下一样。选中栏和弹窗不管头部收没收,窗口矮也不特殊处理。不设快捷键:它是偶尔才改的布局开关,不是分拣时一直要按的。
- 内置播放框(点卡片后在右边打开的 B站 页面)没有顶栏。从上往下是:收藏夹的「视频标签」行、「UP 标签」行,然后是 B站 页面。关闭是播放框右上角骑在边框上的圆形 ×(`#viewerCloseBtn`,一半在框外,tooltip「关闭播放(Esc)」),或者按 Esc。标签行右边留出空,× 不压住标签。760 及以下内置播放框占掉列表的位置,× 还在同一个角上。
+- 封面上只放时长、观看进度和「看完了」,不放 AI 判断。2026-11-14 用户定:判断行的徽标和右边按钮的颜色已经在说同一个结论。
+- 当前卡片(键盘和鼠标所在的那张)只有 2px 深色框,没有阴影,和选中、播放两种状态一样只靠框。AI 没给判断时「保留」和「取消收藏」是同一种按钮样子。
+- 关注的 UP 卡片和收藏夹卡片同一套间距(卡片之间 12px、内边距 12px)。关注列表上面的普通提示只有灰字和按钮,没有框;警告才有琥珀框。
#### 定下来的决定(改之前先问用户)
- 第 3、4 行开头有灰色大字「状态」「标签」,行与行之间有间距和细线。为什么:宽屏下一行两头离得太远,两行胶囊容易看混。
- 第 3 行「全部」后面有竖线,组和组之间也有竖线。
-- 卡片上不放勾选框:蓝框加蓝底已经够看出选中,圆勾在小封面上会压住 AI 判断标签和「看到 X%」标签(2026-10-09 去掉)。
+- 卡片上不放勾选框:蓝框加蓝底已经够看出选中,圆勾在小封面上会压住 AI 判断标签和「看到 X%」标签(2026-10-09 去掉;封面上的 AI 判断标签 2026-11-14 也去掉了)。
- 头部只由按钮收起和展开,不随滚动变(2026-10-14 用户定);封面不缩,静态排版不变。为什么:屏幕高度留给卡片,同时随时能操作筛选。头部放的是排序、筛选、标签这些长期设置,收不收由你定,跟着滚动自己变反而打断你。
- 内置播放框的关闭是右上角骑在边框上的圆形 ×,不放在标签行末尾(会像关闭那一行)。
@@ -389,7 +401,8 @@ B站 页面上的 AI 判断照上表:值得留绿、可清理红、拿不准
- 头部三行是一张白卡,和分拣台的一样(`--surface` 底、`--border` 描边、`--radius-lg` 圆角),上面留 14px、左右 16px(600 及以下都是 8px),行与行之间一道 `--border` 细线。第 2、3 行开头的行名、筛选片、右边的按钮和分拣台第 3、4 行一样。
- 卡片是分拣台的卡片:`--radius-lg` 圆角,卡片之间 12px,选中是蓝框加蓝底。标题 15px、字重 500,信息行 `--faint` 12px。星标单独一列,标题、信息行和正文左边对齐;★ 用 `--star` 黄,对着标题第一行居中。
-- 卡片右上角的按钮是两列一样宽的网格,继续问是灰底;「删除 AI 对话」是 `.quiet` 红字。
+- 卡片右上角的按钮是两列一样宽的网格,继续问是灰底;「删除 AI 对话」是 `.quiet` 红字,12px、22px 高,和同一行的「查看 N 轮问答」一样大。
+- 第 1 行的状态文字和弹窗的状态行是同一套:进行中是转圈,失败是红框,其余是绿点加粗。2026-11-14 用户定。
- 「全选」跟在第 2 行的状态片后面、同一行最右,放不下才换到下一行靠右。为什么:分拣台 760 及以下让状态片占满一行、按钮另起一行,是因为它的状态片多;摘读记录只有两三个,另起一行会空出一条。2026-10-22 用户定
- 760 及以下:两行按钮挪到正文和问答那一行下面,从正文左边开始,标题占满整行;「删除 AI 对话」照旧在问答那一行最右。第 1 行放不下时,搜索框和「清空全部 AI 对话」一起换到下一行,搜索框占满剩下的宽度。
diff --git a/extension/badges.css b/extension/badges.css
index 22ef60bf..55d57152 100644
--- a/extension/badges.css
+++ b/extension/badges.css
@@ -65,8 +65,8 @@ span.mdg-badge.mdg-above { display: flex !important; max-width: 100%; margin: 0
gap: 8px;
margin-top: 8px;
padding: 6px 10px;
- border-left: 3px solid #7c3ed1;
- border-radius: 6px;
+ border-left: 2px solid #7c3ed1;
+ border-radius: 0 6px 6px 0;
background: rgba(124, 62, 209, 0.07);
font-size: 15px;
line-height: 22px;
@@ -186,8 +186,8 @@ span.mdg-ups.mdg-ups-line[data-mid] { display: flex !important; max-width: 100%;
gap: 6px;
margin-bottom: 8px;
padding: 8px 12px;
- border-left: 3px solid #7c3ed1;
- border-radius: 6px;
+ border-left: 2px solid #7c3ed1;
+ border-radius: 0 6px 6px 0;
background: var(--bg1, #fff);
color: var(--text2, #61666d);
font: 13px/18px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
diff --git a/extension/content.css b/extension/content.css
index 0edb01e9..bacc387d 100644
--- a/extension/content.css
+++ b/extension/content.css
@@ -262,7 +262,7 @@ html[data-mdg-reader-mode="1"][data-mdg-reader-theme="dark"] {
display: flex;
inset: 0;
justify-content: center;
- padding: 18px;
+ padding: var(--mdg-space-4);
position: fixed;
z-index: 2147483647;
}
@@ -271,17 +271,17 @@ html[data-mdg-reader-mode="1"][data-mdg-reader-theme="dark"] {
background: var(--mdg-surface);
border: 1px solid var(--mdg-border);
border-radius: var(--mdg-radius-lg);
- box-shadow: 0 20px 60px rgba(15, 23, 42, 0.22);
+ box-shadow: var(--mdg-lift);
color: var(--mdg-text);
font-family: var(--mdg-font-sans);
max-width: 360px;
- padding: 18px;
- width: calc(100vw - 36px);
+ padding: var(--mdg-space-4);
+ width: calc(100vw - 32px);
}
.mdg-confirm-title {
font-size: var(--mdg-fs-lg);
- font-weight: 700;
+ font-weight: 600;
margin-bottom: var(--mdg-space-2);
}
@@ -300,7 +300,7 @@ html[data-mdg-reader-mode="1"][data-mdg-reader-theme="dark"] {
line-height: 1.45;
max-height: 96px;
overflow: auto;
- padding: 9px 10px;
+ padding: var(--mdg-space-2) var(--mdg-space-3);
word-break: break-all;
}
@@ -308,11 +308,11 @@ html[data-mdg-reader-mode="1"][data-mdg-reader-theme="dark"] {
display: flex;
gap: var(--mdg-space-2);
justify-content: flex-end;
- margin-top: 14px;
+ margin-top: var(--mdg-space-4);
}
.mdg-confirm-actions button {
- height: 34px;
+ height: 32px;
padding: 0 14px;
}
@@ -388,7 +388,7 @@ html[data-mdg-reader-mode="1"][data-mdg-reader-theme="dark"] {
background: var(--mdg-warn-bg);
border: 1px solid var(--mdg-warn-border);
border-radius: var(--mdg-radius-md);
- box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
+ box-shadow: var(--mdg-shadow-float);
color: var(--mdg-warn);
font-family: var(--mdg-font-sans);
font-size: var(--mdg-fs-sm);
@@ -581,7 +581,6 @@ body[data-mdg-reader-mode="1"] .mdg-reading-actions {
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
- margin-right: -20px;
margin-top: -8px;
pointer-events: auto;
transition: opacity 0.2s ease;
@@ -606,10 +605,9 @@ body[data-mdg-reader-mode="1"] .mdg-reading-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 34px;
- height: 34px;
- min-height: 36px;
- padding: 0 14px;
+ width: 36px;
+ height: 36px;
+ padding: 0;
border: 1px solid var(--mdg-border);
border-radius: 999px;
background: var(--mdg-surface);
@@ -747,11 +745,7 @@ body[data-mdg-reader-mode="1"] .mdg-reading-toggle {
align-items: center;
gap: 6px;
width: fit-content;
- min-height: 32px;
- padding: 0 11px;
- border: 1px solid var(--mdg-border);
- border-radius: 999px;
- background: var(--mdg-surface);
+ min-height: 28px;
color: var(--mdg-text);
font-size: var(--mdg-fs-xs);
box-sizing: border-box;
@@ -765,11 +759,11 @@ body[data-mdg-reader-mode="1"] .mdg-reading-toggle input {
body[data-mdg-reader-mode="1"] .mdg-reading-select {
width: auto;
min-width: 118px;
- min-height: 36px;
+ min-height: 28px;
margin: 0;
padding: 0 12px;
border: 1px solid var(--mdg-border);
- border-radius: 999px;
+ border-radius: var(--mdg-radius-md);
background: var(--mdg-surface);
color: var(--mdg-text);
font-size: var(--mdg-fs-xs);
@@ -935,8 +929,9 @@ body[data-mdg-reader-mode="1"] .mdg-reading-item.is-active {
body[data-mdg-reader-mode="1"] .mdg-reading-time {
font-size: var(--mdg-reader-transcript-font-size);
- font-weight: 600;
- color: var(--mdg-text) !important;
+ font-weight: 400;
+ font-variant-numeric: tabular-nums;
+ color: var(--mdg-faint) !important;
padding-top: 2px;
line-height: 1.4;
}
diff --git a/extension/history/history.css b/extension/history/history.css
index 320424d7..00322ddc 100644
--- a/extension/history/history.css
+++ b/extension/history/history.css
@@ -4,15 +4,19 @@ body { padding: 14px 16px 0; }
.rec-head, .records { max-width: 1200px; margin: 0 auto; }
/* ---------- the head: one white card as the 分拣台's, rows split by its thin rules (DESIGN 视觉设计 · 分拣台) ---------- */
-.rec-head { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); }
+.rec-head { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); }
.rec-head > * { padding: 8px 16px; }
.rec-head > * + * { border-top: 1px solid var(--border); }
.rec-row1 { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding-block: 12px; }
/* The search and 清空全部 wrap as one, so the rule never starts a line. */
.rec-find { display: flex; align-items: center; gap: 12px; margin-left: auto; }
-.status { color: var(--muted); font-size: var(--fs-xs); }
+.status { display: inline-flex; align-items: baseline; gap: 5px; color: var(--ok); font-size: var(--fs-xs); font-weight: 600; }
.status:empty { display: none; }
-.status.is-error { color: var(--danger); }
+.status::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: currentColor; transform: translateY(-1px); }
+.status[aria-busy="true"] { border: 0; background: none; font-weight: 400; }
+.status[aria-busy="true"]::before { width: 0.9em; height: 0.9em; margin: 0; background: none; transform: translateY(0.1em); }
+.status.is-error { padding: 3px var(--space-2); border: 1px solid var(--danger-border); border-radius: var(--radius-sm); background: var(--danger-bg); color: var(--danger); font-weight: 400; }
+.status.is-error::before { content: none; }
/* 清空全部 AI 对话: red text, no frame, a thin rule between it and the search. */
.rec-row1 .danger { position: relative; margin-left: 9px; border-color: transparent; background: none; color: var(--danger); }
.rec-row1 .danger:hover { background: var(--danger-bg); }
@@ -70,13 +74,13 @@ summary { cursor: pointer; color: var(--text-2); font-size: var(--fs-xs); }
/* Two rows of two equal columns, right-aligned: 继续问 and 复制 Markdown beside the title, 下载 .md and 写入 Obsidian beside
the meta line. */
-.entry-acts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-left: 8px; }
+.entry-acts { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); margin-left: 8px; }
.entry-acts button { width: 100%; }
/* The 「查看 N 轮问答」 line under the body and the buttons, 删除 AI 对话 at its right end (red text, no frame) and staying
on that line when the 问答 open. */
.entry > .qa { grid-row: 2; grid-column: 2 / -1; display: flex; align-items: flex-start; gap: 8px; }
.qa > details { flex: 1; min-width: 0; }
-.qa > [data-act="delete"] { margin-top: -1px; color: var(--danger); }
+.qa > [data-act="delete"] { height: 22px; color: var(--danger); font-size: var(--fs-xs); }
.qa > [data-act="delete"]:hover { background: var(--danger-bg); color: var(--danger); }
/* 继续问 is each card's main action: a gray fill with plain text (buttons never use blue text, DESIGN 视觉设计 · 颜色). */
button.ask { background: var(--field-bg); }
@@ -91,7 +95,7 @@ button:disabled:has(> .write-label [aria-busy="true"]) { opacity: 0.7; cursor: p
.rec-find { flex: 1 1 320px; min-width: 0; }
.rec-find .search { flex: 1 1 auto; width: auto; min-width: 0; }
/* The buttons go under the body, from its left edge, so nothing squeezes the title. */
- .entry-acts { grid-row: 3; grid-column: 2; justify-self: start; margin: 10px 0 0; }
+ .entry-acts { grid-row: 3; grid-column: 2; justify-self: start; margin: var(--space-2) 0 0; }
}
@media (max-width: 600px) {
body { padding: 8px 8px 0; }
diff --git a/extension/history/history.js b/extension/history/history.js
index b92e4594..63e043a1 100644
--- a/extension/history/history.js
+++ b/extension/history/history.js
@@ -299,7 +299,8 @@ function aiProviderId() {
}
}
const RECENT_CHECK = "在记录上逐个看,不对的按 T 或点「+ 标签」改";
-const aiProgress = (text) => (TagDialogs.ai.progress(aiTags, text), setStatus(text));
+// A batch's line (「AI 正在处理第 1 批…」, a wait) does not start with 正在 but is still in progress.
+const aiProgress = (text) => (TagDialogs.ai.progress(aiTags, text), setStatus(text, false, null, true));
const aiTags = {
who: "记录",
sees: `标题、是视频还是网页、现有标签和你与 AI 的对话(问和答,每条记录最多 ${UI.READING_AI_CHARS} 字);没有对话的记录看分拣台的 AI 总结和备注`,
@@ -385,7 +386,8 @@ async function runAi({ instruction, scope, allowRemove }) {
const last = r.errors.at(-1);
// A stop for a setup or access error says so with its remedy, the dialog open or not, as the 分拣台's banner.
if (r.error) setStatus(r.proposal ? last : UI.aiTagsFailed(last), true, aiRemedy(r));
- else if (open) return;
+ // The dialog shows the result; the last batch's line is over.
+ else if (open) return setStatus("");
else if (r.proposal) setStatus("AI 打标签已完成,按 I 或点「✦ AI 打标签」确认建议");
else setStatus(UI.aiTagsFailed(last), true);
}
@@ -642,14 +644,14 @@ function render() {
// 「正在…」 lines are in progress: aria-busy grays them (tokens.css) until the result replaces them.
// error: a failure the user must handle (red); action: { label, run }, its remedy button after the text.
-function setStatus(text, error = false, action = null) {
+function setStatus(text, error = false, action = null, busy = text.startsWith("正在")) {
els.status.textContent = text;
if (action) {
const btn = Object.assign(document.createElement("button"), { type: "button", className: "act-btn", textContent: action.label, onclick: action.run });
els.status.append(" ", btn);
}
els.status.classList.toggle("is-error", error);
- if (text.startsWith("正在")) els.status.setAttribute("aria-busy", "true");
+ if (busy) els.status.setAttribute("aria-busy", "true");
else els.status.removeAttribute("aria-busy");
}
diff --git a/extension/options.css b/extension/options.css
index bc6dd912..af285be8 100644
--- a/extension/options.css
+++ b/extension/options.css
@@ -58,7 +58,7 @@ input[type="password"],
select {
width: 100%;
min-width: 0;
- height: 34px;
+ height: var(--control-h);
padding: 0 10px;
}
@@ -68,7 +68,7 @@ select {
input[type="number"] {
width: 72px;
- height: 34px;
+ height: var(--control-h);
padding: 0 8px;
}
@@ -275,10 +275,10 @@ h1 .brand-mark {
text-decoration: none;
}
-.toc a[aria-current="true"],
-.toc a[aria-current="true"] .ico {
- background: var(--action-bg);
- color: var(--link);
+.toc a[aria-current="true"] {
+ background: var(--surface-muted);
+ box-shadow: inset 3px 0 0 var(--accent);
+ color: var(--text);
font-weight: 600;
}
@@ -308,9 +308,9 @@ main {
align-items: center;
gap: 10px;
margin: 0;
- padding: 6px 10px;
+ padding: 6px 23px;
border: 1px solid var(--danger-border);
- border-radius: var(--radius-sm);
+ border-radius: var(--radius-lg);
background: var(--danger-bg);
color: var(--danger);
font-size: var(--fs-sm);
@@ -322,7 +322,7 @@ main {
.noai {
margin: 0;
- padding: 12px 16px;
+ padding: 12px 24px;
border-radius: var(--radius-lg);
background: var(--surface);
color: var(--text-2);
@@ -456,8 +456,7 @@ input.switch:disabled {
.kind-tabs {
display: flex;
gap: 32px;
- margin-top: 8px;
- border-top: 1px solid var(--border);
+ margin-top: var(--space-4);
border-bottom: 1px solid var(--border);
}
@@ -676,7 +675,8 @@ details.adv[open] > summary::before {
margin-bottom: 8px;
}
-.ai-provider-row :is(input, select) {
+.ai-provider-row :is(input, select),
+.ai-provider-test {
height: var(--control-h);
font-size: var(--fs-sm);
}
@@ -855,8 +855,12 @@ details.adv[open] > summary::before {
}
/* Obsidian */
+.guide-box + .row {
+ border-top: 0;
+}
+
.guide-box {
- margin: 4px 0 8px;
+ margin: 4px 0 var(--space-3);
padding: 10px 16px;
border-radius: var(--radius-md);
background: var(--bg);
@@ -916,15 +920,31 @@ details.adv[open] > summary::before {
color: var(--text);
}
+/* The Obsidian key with 测试连接 beside it; the answer goes under the key's name. */
.test-line {
display: flex;
align-items: center;
- gap: 12px;
- font-size: var(--fs-sm);
+ gap: var(--space-2);
+}
+
+.test-line .key {
+ flex: 1;
+ min-width: 0;
+}
+
+.test-line button {
+ height: var(--control-h);
+}
+
+.test-result {
color: var(--ok);
}
-.test-line [data-error="true"] {
+.test-result:empty {
+ display: none;
+}
+
+.test-result[data-error="true"] {
color: var(--danger);
}
@@ -954,6 +974,11 @@ button.var .mono {
}
/* Data */
+/* In a row the row's own padding is the gap below. */
+.row table.data {
+ margin-bottom: 0;
+}
+
table.data {
width: 100%;
margin: 4px 0 20px;
diff --git a/extension/options.html b/extension/options.html
index 20dd8537..10104bde 100644
--- a/extension/options.html
+++ b/extension/options.html
@@ -279,12 +279,12 @@
MCP
打开后,Claude Code、Cursor、Codex 等 Agent 能通过本机的 MCP 服务用你的登录:搜索和读视频(字幕、评论、画面);读你的 B站收藏夹、关注、历史和稍后再看,以及 YouTube 的稍后观看、赞过的视频、订阅、历史记录和 YouTube 播放列表;改它们:移动、复制、移出收藏夹,新建、改名、删除收藏夹,写分拣台的 AI 判断、保留、备注和标签。把视频加进或移出稍后再看和 YouTube 播放列表。每次修改都记在下面,可以撤销。不含 API Key 和 AI 对话。开着时插件每分钟试连一次本机 127.0.0.1:47823,连上后插件后台会一直运行,不用时关掉。
-
+
安装在 Agent 里添加 MCP 服务,要先装好 Node 20 或更高版本。这样装的服务每次随 Agent 启动取最新版。Agent 说 MCP 服务比插件旧时(以前按本地文件夹装的会这样),用这里的命令重新添加一次,再重启 Agent。
-
+
Agent 最近的修改B站和 YouTube 上的修改留最近 100 次,分拣台里的修改另留最近 100 次。撤销 B站上的修改会在 B站改回去:移走的移回去,移出收藏夹的加回原收藏夹。
@@ -317,17 +317,16 @@
Obsidian
-
-
-
diff --git a/extension/popup.css b/extension/popup.css
index dc3f7e65..db36cf20 100644
--- a/extension/popup.css
+++ b/extension/popup.css
@@ -340,7 +340,6 @@ body {
}
.props {
- border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
padding: var(--space-2);
diff --git a/extension/sidepanel.css b/extension/sidepanel.css
index 8fd38366..f5f0873e 100644
--- a/extension/sidepanel.css
+++ b/extension/sidepanel.css
@@ -346,9 +346,9 @@ body {
.sp-prev {
flex-shrink: 0;
- margin: 8px 14px 6px;
+ margin: var(--space-3) var(--space-3) 0;
padding-left: 2px;
- border: 1px solid var(--border-strong);
+ border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
overflow: hidden;
@@ -488,7 +488,7 @@ body {
.sp-messages {
flex: 1;
overflow-y: auto;
- padding: 12px 14px 14px;
+ padding: var(--space-3) var(--space-3) 14px;
display: flex;
flex-direction: column;
gap: 14px;
@@ -693,9 +693,7 @@ body:has(#spInput:not(:placeholder-shown)) .sp-summary-btn {
display: flex;
justify-content: flex-start;
gap: 4px;
- margin-top: 12px;
- padding-top: 8px;
- border-top: 1px solid var(--border);
+ margin-top: var(--space-2);
}
.sp-msg-copy-btn {
@@ -876,9 +874,16 @@ body:has(#spInput:not(:placeholder-shown)) .sp-summary-btn {
font-size: var(--fs-xs);
}
+/* A reply that failed before its first word: the error box stands alone, without the bubble around it. */
+.sp-msg-assistant:has(> .sp-msg-error:first-child) {
+ background: none;
+ padding: 0;
+}
+
.sp-grant-btn {
margin-top: 6px;
padding: 6px 10px;
+ border-radius: var(--radius-sm);
font-size: var(--fs-xs);
}
@@ -1158,7 +1163,7 @@ body:has(#spInput:not(:placeholder-shown)) .sp-summary-btn {
grid-template-columns: minmax(0, 1fr) 24px;
gap: 6px;
align-items: center;
- border: 1px solid var(--border);
+ border: 1px solid transparent;
border-radius: var(--radius-md);
background: var(--surface-muted);
padding: 4px;
@@ -1201,7 +1206,7 @@ body:has(#spInput:not(:placeholder-shown)) .sp-summary-btn {
align-items: baseline;
gap: 4px;
min-width: 0;
- font-size: var(--fs-xs);
+ font-size: var(--fs-sm);
line-height: 1.35;
}
@@ -1217,11 +1222,11 @@ body:has(#spInput:not(:placeholder-shown)) .sp-summary-btn {
flex: 0 0 auto;
white-space: nowrap;
color: var(--text-2);
- font-size: 11px;
+ font-size: var(--fs-xs);
}
.sp-history-meta {
- font-size: 11px;
+ font-size: var(--fs-xs);
line-height: 1.3;
color: var(--muted);
white-space: nowrap;
diff --git a/extension/triage/fav-folders.js b/extension/triage/fav-folders.js
index 7777154e..2b911d43 100644
--- a/extension/triage/fav-folders.js
+++ b/extension/triage/fav-folders.js
@@ -73,13 +73,10 @@ function coverHtml(it) {
const faint = !seen && known && S.seenCfg.mark;
const label = seenWords(known, seen);
const mark = seen ? `
${label}${label}` : faint ? `
${label}` : "";
- // Once the user has decided, their decision replaces the AI's verdict label; the AI's reason and summary stay.
- const v = isProcessed(it.bvid) ? { verdict: "none" } : verdictOf(it);
- const tag = S.analyzing.has(it.bvid) ? `
${ANALYZING}` : VERDICTS[v.verdict] ? `
${VERDICTS[v.verdict]}` : "";
const dur = it.duration ? `
${fmtDuration(it.duration)}` : "";
// A real link for right-click 在新标签页中打开 and ⌘-click; a plain click selects the card. Out of the tab order: the title
// is the same link for the keyboard, and the title is what plays.
- return `
${pic}${tag}${dur}${mark}${p ? `` : ""}`;
+ return `
${pic}${dur}${mark}${p ? `` : ""}`;
}
async function loadTagsAndCriteria() {
diff --git a/extension/triage/follow.css b/extension/triage/follow.css
index ecc4b9d2..2a752067 100644
--- a/extension/triage/follow.css
+++ b/extension/triage/follow.css
@@ -25,15 +25,15 @@ body:not(.follow-mode) :is(.fw-side-slot, .follow-help) { display: none !importa
.fw-side-slot { display: none; }
.fw-side-select { height: 32px; max-width: 200px; font-weight: 600; }
-.fw-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; margin: 0 -8px; padding: calc(var(--head-h, 0px) + 8px) 8px 90px; }
-.fw-hint { margin: 0; padding: 8px 12px; border: 1px solid var(--info-border); border-radius: var(--radius-md); background: var(--info-bg); color: var(--text-2); line-height: 1.6; }
-.fw-hint.warn { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn); }
+.fw-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-3); margin: 0 -8px; padding: calc(var(--head-h, 0px) + 8px) 8px 80px; }
+.fw-hint { margin: 0; padding: 0 var(--space-3); color: var(--text-2); line-height: 1.6; }
+.fw-hint.warn { padding: 8px 12px; border: 1px solid var(--warn-border); border-radius: var(--radius-md); background: var(--warn-bg); color: var(--warn); }
.fw-hint button { height: 26px; margin-left: 8px; font-size: var(--fs-xs); }
/* ---------- UP card: select box over the avatar | name, status, meta, sign, 3 titles, tags | ★ ---------- */
-.fw-up { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 14px; padding: 12px 14px; }
-/* Hover as on 收藏夹's cards: the dark ring and the lift; the select box and 「+ 标签」 show. */
-.fw-up:hover:not(.selected, .playing) { outline: 2px solid var(--text-2); outline-offset: -1px; box-shadow: var(--lift); }
+.fw-up { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: var(--space-4); padding: var(--space-3); }
+/* Hover as on 收藏夹's cards: the dark ring; the select box and 「+ 标签」 show. */
+.fw-up:hover:not(.selected, .playing) { outline: 2px solid var(--text-2); outline-offset: -1px; }
.fw-avatar { width: 56px; height: 56px; border-radius: 50%; overflow: hidden; background: var(--field-bg); }
.fw-avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.fw-name-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
diff --git a/extension/triage/triage.css b/extension/triage/triage.css
index f60ba62e..231ee359 100644
--- a/extension/triage/triage.css
+++ b/extension/triage/triage.css
@@ -74,7 +74,7 @@ select { height: 28px; max-width: 240px; }
/* Whatever is running (粗看, 细看, unfavorite batch, loading, refresh, tag AI, a wait) shows here on every tab. */
/* What is running: gray (blue is for the next step, DESIGN 视觉设计 · 颜色); a wait turns it amber. */
-.activity { display: inline-flex; align-items: center; gap: 8px; height: 28px; padding: 0 4px 0 12px; border: 1px solid var(--action-border); border-radius: 15px; background: var(--action-bg); color: var(--link); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 0; }
+.activity { display: inline-flex; align-items: center; gap: 8px; height: 28px; padding: 0 4px 0 12px; border: 1px solid var(--action-border); border-radius: var(--radius-pill); background: var(--action-bg); color: var(--link); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 0; }
/* A narrow window cuts the line, not the countdown or buttons after it; the pill's title is the whole line. */
.activity-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.activity > :not(.activity-text) { flex: none; }
@@ -91,7 +91,7 @@ select { height: 28px; max-width: 240px; }
/* 已刷新 is a pill; its full text and list open below it, right-aligned to it, and slide right to stay on screen. */
.sync-wrap { position: relative; anchor-scope: --sync; }
.sync-pill { anchor-name: --sync; }
-.pill { height: 30px; padding: 0 12px; border-radius: 15px; white-space: nowrap; }
+.pill { height: 28px; padding: 0 12px; border-radius: var(--radius-pill); white-space: nowrap; }
.sync-pill::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: var(--action); vertical-align: 1px; }
.sync-pill.warn { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn); }
.sync-pill.warn::before { background: var(--warn); }
@@ -217,7 +217,7 @@ select { height: 28px; max-width: 240px; }
:is(.folder-head, .tabrow, .stagebar, .tagbar)::before { content: ""; position: absolute; inset: 0; z-index: -1; border: 1px solid var(--border); border-width: 0 1px; background: var(--surface); }
.folder-head::before { border-top-width: 1px; border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
:is(.tabrow, .stagebar)::before { border-bottom-width: 1px; }
-.tagbar::before { border-bottom-width: 1px; border-radius: 0 0 var(--radius-lg) var(--radius-lg); box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); }
+.tagbar::before { border-bottom-width: 1px; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.viewer, .viewer-close { transition: margin-top 0.15s; }
.head-folded :is(.folder-head, .stagebar, .tagbar) { visibility: hidden; opacity: 0; }
/* --head1-h is measured from .main's padding edge, so the slim bar keeps the gap above the card. */
@@ -256,7 +256,7 @@ select { height: 28px; max-width: 240px; }
.tabs:empty { display: none; }
/* Row 2: the tabs; search and the mode's buttons sit on the right of the same line. */
.tabrow { display: flex; flex-wrap: wrap; align-items: center; column-gap: 24px; }
-.row-tools { display: flex; align-items: center; gap: 2px; margin-left: auto; padding: 6px 0; }
+.row-tools { display: flex; align-items: center; gap: 2px; margin: 0 -8px 0 auto; padding: 6px 0; }
/* While it reads, 刷新 gives its place to the progress pill: one thing on screen, not two. */
.row-tools:has(.activity:not([hidden])) .act-btn[aria-busy="true"] { display: none; }
.tabs { display: flex; flex: 0 1 auto; min-width: 0; min-height: 40px; align-items: center; gap: 2px; }
@@ -286,7 +286,7 @@ select { height: 28px; max-width: 240px; }
.stagebar .step-actions { flex-wrap: nowrap; }
.stagebar .class-filter, .tagbar .tagfilter { min-height: 28px; }
.tagbar > :last-child { grid-column: 2; }
-.tags-acts { display: flex; flex: none; flex-wrap: nowrap; justify-content: flex-end; gap: 2px; }
+.tags-acts { display: flex; flex: none; flex-wrap: nowrap; justify-content: flex-end; gap: 2px; margin-right: -8px; }
/* Row 3's groups (shared.js stateGroup), split by a thin rule. A group wraps whole (inside only when it alone is too
wide); the rule trails its group. */
/* Rows 3 and 4 start with their name (shared.js labeledRow), a gray row heading of one width, so both rows' first pills
@@ -379,10 +379,10 @@ select { height: 28px; max-width: 240px; }
.sel-actions .danger::before { content: ""; position: absolute; left: -12px; top: 8px; height: 16px; border-left: 1px solid var(--border-strong); }
.list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; margin: 0 -8px; padding: calc(var(--head-h, 0px) + 8px) 8px 80px; }
-/* Two card states. Current (hover or J/K) is the lifted box; selected (a click on the card or X) is the blue outline and tint. Both can show at once. */
+/* Two card states. Current (hover or J/K) is the dark ring; selected (a click on the card or X) is the blue outline and tint. Both can show at once. */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); transition: border-color 0.12s, box-shadow 0.12s; }
-/* Current: a dark ring plus the lift, so it reads at a glance; selected (below) swaps the ring for blue. */
-.card.focused { outline: 2px solid var(--text-2); outline-offset: -1px; box-shadow: var(--lift); }
+/* Current: a dark ring, so it reads at a glance; selected (below) swaps the ring for blue. */
+.card.focused { outline: 2px solid var(--text-2); outline-offset: -1px; }
.card.selected { outline: 2px solid var(--action); outline-offset: -1px; background: var(--action-bg); }
/* Playing in the viewer: pink, where you are. Blue stays for 选中. */
.card.playing:not(.selected) { outline: 2px solid var(--accent); outline-offset: -1px; }
@@ -394,16 +394,11 @@ select { height: 28px; max-width: 240px; }
.empty-state button { display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 20px; }
.empty-state button svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
-/* ---------- cover: progress bar and marks, the duration bottom-right, the AI verdict top-left after the select box ---------- */
-.cover-wrap { position: relative; display: block; text-decoration: none; align-self: start; width: 100%; aspect-ratio: 16 / 10; border-radius: var(--radius-md); overflow: hidden; background: var(--action-bg); color: var(--on-image); }
+/* ---------- cover: progress bar and marks, the duration bottom-right ---------- */
+.cover-wrap { position: relative; display: block; text-decoration: none; align-self: start; width: 100%; aspect-ratio: 16 / 10; border-radius: var(--radius-md); overflow: hidden; background: var(--surface-muted); color: var(--on-image); }
.cover-wrap::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 38%; background: linear-gradient(transparent, var(--cover-scrim)); pointer-events: none; }
.cover { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.cover-dur { position: absolute; right: 8px; bottom: 6px; z-index: 1; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
-.cover-tag { position: absolute; left: 6px; top: 6px; z-index: 1; height: 20px; padding: 0 6px; border-radius: 4px; font-size: var(--fs-xs); font-weight: 600; line-height: 20px; white-space: nowrap; }
-.cover-tag.keep { background: var(--ok-bg); color: var(--ok); }
-.cover-tag.drop { background: var(--cover-chip); color: var(--on-image); }
-.cover-tag.unsure { background: var(--on-image); color: var(--warn); }
-.cover-tag.running { background: var(--cover-chip); color: var(--on-image); font-weight: 400; }
/* 观看进度: html[data-seen-style] picks the corner tag or the veil (设置页「看完了标记样式」). */
.seen-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; height: 3px; background: var(--cover-track); pointer-events: none; }
.seen-bar i { display: block; height: 100%; background: var(--accent); }
@@ -467,7 +462,7 @@ html[data-seen-style="veil"] .cover-wrap.seen .seen-veil { display: flex; }
Touch has no hover and no keys, so there they always show (both modes). */
@media (hover: hover) { .card:not(.focused, :focus-within) .more button:not(.on) { visibility: hidden; } }
.note { width: 100%; max-width: 560px; margin-top: 6px; padding: 4px 8px; border: 0; border-radius: var(--radius-sm); background: var(--bg); resize: vertical; min-height: 28px; font-size: var(--fs-sm); color: var(--text-2); field-sizing: content; }
-.note-add { font-size: var(--fs-xs); height: auto; }
+.note-add { font-size: var(--fs-xs); height: auto; margin-left: -8px; }
.group-head { margin-top: 10px; padding: 2px 0; color: var(--muted); font-size: var(--fs-xs); }
.group-head button { height: 24px; padding: 0 8px; font-size: inherit; }
.title-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
@@ -480,14 +475,12 @@ button.icon { width: 26px; height: 24px; padding: 0; }
.list .pair > button { position: absolute; right: 12px; width: 112px; height: 32px; padding: 0; justify-content: center; font-size: var(--fs-sm); }
.list .pair > button:first-child { top: calc(50% - 36px); }
.list .pair > button:last-child { top: calc(50% + 4px); }
-.list .pair > button:last-child:not(.solid) { border-color: transparent; background: var(--field-bg); }
.list .actions .more { margin-left: auto; }
/* Beside the viewer the list is narrow: the smaller cover, and the buttons back in the foot row, the pair first. */
.main.viewing .list .card { grid-template-columns: 128px minmax(0, 1fr); gap: 10px; padding-right: 12px; }
.main.viewing .list .actions { display: flex; justify-content: flex-end; }
.main.viewing .list .pair { display: flex; }
.main.viewing .list .pair > button { position: static; width: auto; height: 26px; padding: 0 8px; font-size: var(--fs-xs); }
-.main.viewing .list .pair > button:last-child:not(.solid) { border-color: var(--border-strong); background: var(--surface); }
.main.viewing .list .actions .more { margin-left: 0; }
.main.viewing .list .key { display: none; }
.shell:has(.main.viewing) { grid-template-columns: minmax(0, 1fr); }
@@ -538,7 +531,7 @@ button.icon { width: 26px; height: 24px; padding: 0; }
.viewer-line { --row-label-w: 4.5em; padding-left: calc(12px + var(--row-label-w) + 8px); }
.viewer-line > .row-label { margin-left: calc(-1 * var(--row-label-w) - 8px); font-size: inherit; line-height: inherit; }
.viewer-line .chip[data-pick], .viewer-line .chip[data-vtag] { cursor: pointer; }
-.viewer { display: flex; flex-direction: column; min-height: 0; min-width: 0; margin-top: calc(var(--head-vis) + 8px); background: var(--surface); border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
+.viewer { display: flex; flex-direction: column; min-height: 0; min-width: 0; margin-top: calc(var(--head-vis) + 8px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
/* No top bar. × rides on the panel's top-right corner, half outside (its own grid item, so .viewer's overflow does not
clip it); the lines keep their right end clear of it. The panel starts 8px down, as the list's first card does, so
the × clears row 4's buttons. */
@@ -561,7 +554,7 @@ button.icon { width: 26px; height: 24px; padding: 0; }
flex-direction: column;
background: var(--surface);
border: 1px solid var(--border);
- border-radius: 12px;
+ border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
}
.basket.collapsed { width: auto; border-color: var(--border-strong); border-radius: var(--radius-md); }
@@ -569,7 +562,7 @@ button.icon { width: 26px; height: 24px; padding: 0; }
.listpane:has(.selbar) .basket { margin-bottom: 84px; }
.basket.collapsed .basket-body { display: none; }
.basket-toggle { font-weight: 600; border: 0; background: transparent; border-radius: var(--radius-md); }
-.basket:not(.collapsed) .basket-toggle { border-top: 1px solid var(--border); border-radius: 0 0 12px 12px; }
+.basket:not(.collapsed) .basket-toggle { border-top: 1px solid var(--border); border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.basket:not(.collapsed) .chev { display: inline-block; transform: rotate(180deg); }
.basket.bump { animation: basket-bump 0.5s ease; }
@keyframes basket-bump { 40% { transform: scale(1.08); } }
@@ -630,8 +623,6 @@ button.icon { width: 26px; height: 24px; padding: 0; }
.keys kbd { min-width: 1.6em; padding: 0 4px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface); font: 600 var(--fs-xs)/1.3 ui-monospace, Menlo, monospace; text-align: center; }
@media (max-width: 899px) {
- .card { grid-template-columns: 96px minmax(0, 1fr); }
- .cover { width: 96px; height: 54px; }
.toast { bottom: 64px; }
}