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
3 changes: 2 additions & 1 deletion extension/entry-matrix.selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ const ALLOW = [
// 3c: the gone video's sentence; whether 重试 shows is remedies'.
["content.js", "MdgSites.isBiliVideoGone(state.readCode) ? MdgErrors.LABEL.videoGone", "说法:视频已失效"],
["sidepanel.js", "MdgSites.isBiliVideoGone(resp?.code) ? MdgErrors.LABEL.videoGone", "说法:视频已失效"],
["sidepanel.js", "const gone = !ok && MdgSites.isBiliVideoGone(response?.code);", "说法:VIDEO_GONE(只能查看,不能继续提问)"],
["sidepanel.js", "const gone = MdgSites.isBiliVideoGone(response?.code);", "说法:VIDEO_GONE(只能查看,不能继续提问)"],
["sidepanel.js", "MdgSites.isBiliVideoGone(failed?.code) ? VIDEO_GONE", "说法:VIDEO_GONE(读取失败存在 view().failed,静默读取也只能查看)"],
["sidepanel.js", "MdgSites.isBiliVideoGone(response?.code) ? null : undefined", "已失效的视频补全标题时不再重查,不显示错误"]
];
const used = ALLOW.map(() => 0);
Expand Down
17 changes: 10 additions & 7 deletions extension/sidepanel-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@

// live: the active tab (context is its payload, or the placeholder of a video followed from its URL alone).
// conv: the shown conversation. pinned is its own context's description once it has one (saved, opened from a
// reference, or asked in); resolved is that context loaded; failed: loading it failed, so it is no longer pending.
// reference, or asked in); resolved is that context loaded; failed: loading it failed, so it is no longer pending:
// { code (the read's error code), text (said under it; "" when the load was silent) }.
// fill: its AI 总结 prompt is still to fill. unanswered: the last question when its reply failed or stopped before a
// word came, kept only to draw it again. convs (MdgCommon.latest): the shown conversation's run; showing another one
// ends it, so a load or a drawing begun on the one before can tell.
Expand All @@ -192,7 +193,7 @@
function create() {
const s = {
live: { tabUrl: "", context: null, key: "", error: "", lastVideoUrl: "" },
conv: { id: "", pinned: null, messages: [], resolved: null, failed: false, fill: false, unanswered: null, autoSend: null },
conv: { id: "", pinned: null, messages: [], resolved: null, failed: null, fill: false, unanswered: null, autoSend: null },
convs: MdgCommon.latest(),
turn: idleTurn(),
detached: new Map(),
Expand Down Expand Up @@ -229,7 +230,7 @@
function view(s) {
const context = shown(s);
const key = shownKey(s);
const { id, pinned, messages, resolved, unanswered } = s.conv;
const { id, pinned, messages, resolved, failed, unanswered } = s.conv;
const target = shownPartUrl(s);
return {
context,
Expand All @@ -239,6 +240,7 @@
pinned,
messages,
resolved,
failed,
run: s.convs.current,
unanswered,
turn: s.turn.phase,
Expand Down Expand Up @@ -292,7 +294,7 @@
effects.push({ type: "draft" });
}
s.convs.next();
s.conv = { id: "", pinned: null, messages: [], resolved: null, failed: false, fill: false, unanswered: null, autoSend: null };
s.conv = { id: "", pinned: null, messages: [], resolved: null, failed: null, fill: false, unanswered: null, autoSend: null };
effects.push(...reattach(s, conversation));
if (conversation) {
s.conv.id = conversation.id;
Expand Down Expand Up @@ -604,15 +606,15 @@
case "GRANT_DENIED":
s.conv.autoSend = null;
return [];
// A conversation's own context loaded (payload) or failed; a load started for an earlier conversation (its run
// ended) is ignored.
// A conversation's own context loaded (payload) or failed (code, text: conv.failed); a load started for an
// earlier conversation (its run ended) is ignored.
case "CONTEXT_RESOLVED": {
if (!event.run?.alive || !s.conv.pinned) {
return [];
}
if (!event.payload) {
const wasPending = Boolean(shown(s)?.pending);
s.conv.failed = true;
s.conv.failed = { code: event.code, text: event.text || "" };
return [{ type: "resolveFailed", wasPending }];
}
const payload = event.payload;
Expand All @@ -625,6 +627,7 @@
contextRef: buildConversationContextRef(payload)
};
s.conv.resolved = payload;
s.conv.failed = null;
return [{ type: "resolved" }];
}
case "OPEN":
Expand Down
14 changes: 14 additions & 0 deletions extension/sidepanel-state.selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,20 @@ function panel({ tab = video("BV1aa"), list = [] } = {}) {
p.dispatch({ type: "OPEN", id: "h" });
assert.deepStrictEqual(plain(p.dispatch({ type: "CONTEXT_RESOLVED", run: p.view().run, payload: null })), [{ type: "resolveFailed", wasPending: true }]);
assert.strictEqual(p.view().context.pending, false);
// The failure is the conversation's: kept for redraws, a silent one without words (still gone); one for the
// conversation shown before is ignored, and showing another conversation or a load that works clears it.
assert.strictEqual(p.view().failed.text, "");
const hRun = p.view().run;
p.dispatch({ type: "CONTEXT_RESOLVED", run: hRun, payload: null, code: -404, text: "" });
assert.deepStrictEqual(plain(p.view().failed), { code: -404, text: "" });
p.dispatch({ type: "OPEN", id: "g" });
assert.strictEqual(p.view().failed, null);
p.dispatch({ type: "CONTEXT_RESOLVED", run: hRun, payload: null, code: -404, text: "gone" });
assert.strictEqual(p.view().failed, null, "stale");
p.dispatch({ type: "CONTEXT_RESOLVED", run: p.view().run, payload: null, code: "THROTTLED", text: "限流" });
assert.deepStrictEqual(plain(p.view().failed), { code: "THROTTLED", text: "限流" });
p.dispatch({ type: "CONTEXT_RESOLVED", run: p.view().run, payload: video("BV1gg") });
assert.strictEqual(p.view().failed, null);
// The tab's own video: the live context, nothing to resolve.
const q = panel({ list: [saved("a", video("BV1aa"))] });
q.dispatch({ type: "NEW" });
Expand Down
37 changes: 17 additions & 20 deletions extension/sidepanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,9 @@ const REPLY_RUNNING = "上一个回答还在生成,结束后才能继续问";
// Why the shown conversation cannot be asked in now: a web page not read, a video that is gone, or its last reply
// still running out of reach.
function readOnlyReason() {
const { nonVideo, context, replyDetached } = state.view();
const { nonVideo, context, replyDetached, failed } = state.view();
if (replyDetached) return REPLY_RUNNING;
return nonVideo ? pageReason(context) : contextFailure()?.gone ? VIDEO_GONE : "";
return nonVideo ? pageReason(context) : MdgSites.isBiliVideoGone(failed?.code) ? VIDEO_GONE : "";
}

// Why a web page cannot be asked about, by background.js readWebPage's pageAccess; "" once its main text is read. A page
Expand Down Expand Up @@ -1619,9 +1619,9 @@ function renderConversationMessages() {
els.messages.appendChild(node);
appendReplyEnd(node, { ...unanswered, raw: "", saved: false });
}
const failed = nonVideo ? pageReason(state.view().context) : contextFailure()?.text;
const failed = nonVideo ? pageReason(state.view().context) : state.view().failed?.text;
if (failed) {
showConversationContextError(failed, nonVideo ? null : contextFailure().remedy);
showConversationContextError(failed, nonVideo ? null : contextRemedy());
}
renderFollowups();
drawnRun = run;
Expand Down Expand Up @@ -1685,11 +1685,6 @@ async function ensureCurrentContextForSend() {
return true;
}

// The shown conversation's own video failed to load: { run (its conversation's), text (said under it, again at each
// redraw; "" when the load was silent), gone (deleted or hidden on B站: the conversation is only read) }.
let lastContextFailure = null;
const contextFailure = () => (lastContextFailure?.run.alive ? lastContextFailure : null);

// Loads the shown conversation's own context (state decides when: resolve effects). A load the conversation was
// switched away from meanwhile does nothing. notice: the 正在加载… line it takes down when done.
async function hydratePinnedConversationContext({ silent = false, notice = null } = {}) {
Expand All @@ -1716,16 +1711,15 @@ async function hydratePinnedConversationContext({ silent = false, notice = null
error: error?.message || String(error || "")
}));
const ok = Boolean(response?.ok && response.payload);
const [result] = state.dispatch({ type: "CONTEXT_RESOLVED", run, payload: ok ? response.payload : null });
// Deleted or hidden on B站 (as the 分拣台 and 摘读记录 tell it): no retry will bring it back, and the conversation is
// only read. Else a send tries again.
const gone = MdgSites.isBiliVideoGone(response?.code);
const failure = ok ? {} : { code: response?.code, text: silent ? "" : gone ? VIDEO_GONE : `读取原${sourceKind()}失败:${MdgErrors.text(response)}` };
const [result] = state.dispatch({ type: "CONTEXT_RESOLVED", run, payload: ok ? response.payload : null, ...failure });
notice?.remove();
if (!result) {
return false;
}
// Deleted or hidden on B站 (as the 分拣台 and 摘读记录 tell it): no retry will bring it back. Else a send tries again.
const gone = !ok && MdgSites.isBiliVideoGone(response?.code);
const text = gone ? VIDEO_GONE : `读取原${sourceKind()}失败:${MdgErrors.text(response)}`;
const remedy = !ok && MdgErrors.remedies(response?.code).includes("retry") ? contextRemedy(contextRef) : null;
lastContextFailure = ok ? null : { run, text: silent ? "" : text, gone, remedy };
updateContextChip();
syncSendUi();
if (ok) {
Expand All @@ -1736,15 +1730,18 @@ async function hydratePinnedConversationContext({ silent = false, notice = null
suggestionsNode?.replaceChildren();
}
if (!silent) {
showConversationContextError(text, lastContextFailure.remedy);
showConversationContextError(failure.text, contextRemedy());
}
return false;
}

// What helps a saved conversation whose video failed to load (not gone): a site read only from its page (YouTube)
// needs that page open, so 打开视频 opens it in this tab as the chip does; any other gets 重试 (B站限流, the network).
// Either reads it again, and takes the error down once it loads.
function contextRemedy(contextRef) {
// What helps the shown conversation whose video failed to load (by its code; nothing when gone): a site read only
// from its page (YouTube) needs that page open, so 打开视频 opens it in this tab as the chip does; any other gets 重试
// (B站限流, the network). Either reads it again, and takes the error down once it loads.
function contextRemedy() {
const { failed, pinned } = state.view();
if (!MdgErrors.remedies(failed.code).includes("retry")) return null;
const { contextRef } = pinned;
const again = async () => {
if (await hydratePinnedConversationContext()) removeCenteredState();
};
Expand Down
8 changes: 4 additions & 4 deletions extension/sidepanel.selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,13 +580,13 @@ const tick = () => new Promise((r) => setImmediate(r));
// J7: a failure that may pass (a limit, the network) gets 重试, as the tab's own read does; a gone video none. The
// 重试 reads it again and takes the error down once it loads.
assert.deepStrictEqual(remedies, ["", "重试", "重试"]);
const failure = vm.runInContext("contextFailure()", ctx);
assert.strictEqual(failure.remedy.label, "重试", "a redraw shows it again");
const remedy = vm.runInContext("contextRemedy()", ctx);
assert.strictEqual(remedy.label, "重试", "a redraw shows it again");
let centered = 0;
const realRemove = ctx.removeCenteredState;
ctx.removeCenteredState = () => centered++;
ctx.resolveConversationContext = async () => ({ ok: true, payload: video });
await failure.remedy.run();
await remedy.run();
assert.deepStrictEqual([centered, view().resolved?.videoId], [1, video.videoId]);
ctx.removeCenteredState = realRemove;
// J12: a YouTube video reads only from its page: the remedy opens it (in this tab, as the chip does), then reads.
Expand All @@ -601,7 +601,7 @@ const tick = () => new Promise((r) => setImmediate(r));
const steps = [];
ctx.openCurrentContextUrl = async () => steps.push("open");
ctx.resolveConversationContext = async () => (steps.push("read"), { ok: false, error: "x" });
await vm.runInContext("contextFailure()", ctx).remedy.run();
await vm.runInContext("contextRemedy()", ctx).run();
assert.deepStrictEqual(steps, ["open", "read"]);
ctx.openCurrentContextUrl = realOpen;
errors.length = remedies.length = 0;
Expand Down
Loading