pi-web-ui 0.96.0 → 0.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +69 -2
  2. package/README.md +26 -0
  3. package/README.zh-CN.md +25 -0
  4. package/bin/pi-web-ui.mjs +11 -2
  5. package/dist/server/agent-service.js +1195 -120
  6. package/dist/server/client-state.js +89 -1
  7. package/dist/server/compacted-history.js +146 -0
  8. package/dist/server/delegate-mode.js +149 -0
  9. package/dist/server/delegate-task.js +10 -10
  10. package/dist/server/dsh/dsh-agent-service.js +35 -2
  11. package/dist/server/dsh/dsh-client.js +3 -1
  12. package/dist/server/eval-tool.js +3 -1
  13. package/dist/server/goal-review-gate.js +263 -0
  14. package/dist/server/goal-service.js +1225 -445
  15. package/dist/server/index.js +79 -20
  16. package/dist/server/lsp-tool.js +13 -10
  17. package/dist/server/markers/builtins/todo.js +0 -2
  18. package/dist/server/mcp-bridge.js +13 -0
  19. package/dist/server/patch-tool.js +1 -0
  20. package/dist/server/patch-turn-end-boundary.js +77 -0
  21. package/dist/server/permission-preset.js +44 -0
  22. package/dist/server/plan-mode.js +376 -0
  23. package/dist/server/plugin-facilities.js +36 -3
  24. package/dist/server/plugin-installer.js +30 -0
  25. package/dist/server/plugins.js +6 -3
  26. package/dist/server/protocol-version.js +1 -1
  27. package/dist/server/serialize.js +11 -0
  28. package/dist/server/settings-service.js +20 -8
  29. package/dist/server/subagents.js +360 -408
  30. package/dist/server/text-sniff.js +15 -9
  31. package/dist/server/tool-manager.js +48 -4
  32. package/dist/server/update-check.js +11 -3
  33. package/dist/server/wait-subscription-scan.js +5 -1
  34. package/package.json +3 -3
  35. package/themes/zhupi-dark.css +79 -44
  36. package/themes/zhupi.css +79 -44
  37. package/web/dist/assets/{TerminalPanel-DDChcYOh.js → TerminalPanel-CITBqQrb.js} +1 -1
  38. package/web/dist/assets/index-DwToli0J.css +1 -0
  39. package/web/dist/assets/index-oI7UKOqJ.js +368 -0
  40. package/web/dist/index.html +2 -2
  41. package/web/dist/assets/index-Do9RgJC3.js +0 -366
  42. package/web/dist/assets/index-DryOsILO.css +0 -1
@@ -16,6 +16,9 @@
16
16
  * all share one conversation list per project.
17
17
  * PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
18
18
  */
19
+ // MUST be imported before the SDK is loaded so on-disk patches apply cleanly.
20
+ import "./patch-remote-catalog.js";
21
+ import "./patch-turn-end-boundary.js";
19
22
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
20
23
  import { stat } from "node:fs/promises";
21
24
  import { createServer, request as proxyRequest } from "node:http";
@@ -48,7 +51,7 @@ import { validateClientId } from "./ws-client-id.js";
48
51
  import { PendingCommandQueue } from "./ws-pending-queue.js";
49
52
  import { installPack, isKnownPack, listPacks, loadServerStrings, readPackFile, removePack, unloadServerStrings, } from "./locales.js";
50
53
  import { PluginManager, resolvePluginClientFile, } from "./plugins.js";
51
- import { buildPluginJobArgs, inspectInstallSpec, PluginInstaller } from "./plugin-installer.js";
54
+ import { buildPluginJobArgs, confirmPluginInstall, inspectInstallSpec, PluginInstaller } from "./plugin-installer.js";
52
55
  import { syncPluginCatalog } from "./plugin-catalog-sync.js";
53
56
  import { McpBridge } from "./mcp-bridge.js";
54
57
  import { createMcpHotReload } from "./mcp-hot-reload.js";
@@ -1129,22 +1132,14 @@ function findDomConsentById(id) {
1129
1132
  }
1130
1133
  // ---------------------------------------------------------------------------
1131
1134
  // 插件安装的用户确认门(P0):plugin_catalog_sync 的 install:true 与 plugin_job
1132
- // 的 install/update 在真正动安装器之前必须拿到用户确认。第三方页面脚本可以直发
1133
- // 这两类消息,没有这道门就能借 WS 静默安装任意插件。复用 permissionRequester
1134
- // 的同一条「plugin_permission_request 弹窗 + 120s 超时视为拒绝」管线 —— 不新增
1135
- // 协议消息,将安装清单放在 reason 里展示;这里直接调 requester(不写授权表,
1136
- // remember 标志被忽略)。拒绝 / 超时 / 未接弹窗设施(无头 DSH)一律 fail-closed。
1135
+ // 的 install/update 在真正动安装器之前必须拿到用户确认。
1137
1136
  // ---------------------------------------------------------------------------
1138
- async function confirmPluginInstall(items) {
1139
- const ask = pluginMgr.permissionRequester;
1140
- if (!ask)
1141
- return false;
1142
- const list = items.map((x) => `${x.id} ← ${x.source}`).join("\n");
1143
- const ans = await ask("plugin-installer", {
1144
- family: "net",
1145
- reason: `安装确认:将安装/更新以下插件(id ← source):\n${list}\n拒绝或 120 秒未确认则不安装。`,
1137
+ async function confirmPluginInstallHelper(items) {
1138
+ return confirmPluginInstall(items, {
1139
+ permGrants: pluginMgr.permGrants,
1140
+ permissionRequester: pluginMgr.permissionRequester,
1141
+ onGrantsChanged: pushPluginPermissions,
1146
1142
  });
1147
- return ans.ok === true;
1148
1143
  }
1149
1144
  // 内置定时任务(issue #184):全局 <dataDir>/scheduler-tasks.json,TTL 与
1150
1145
  // client-state 同级;Agent 工具建的任务优先唤醒发起对话(issue #193:
@@ -1641,9 +1636,20 @@ wss.on("connection", (ws) => {
1641
1636
  return;
1642
1637
  }
1643
1638
  switch (msg.type) {
1644
- case "prompt":
1639
+ case "prompt": {
1640
+ const hasAttach = Boolean(msg.attachments && msg.attachments.length > 0);
1641
+ if (!msg.text?.trim() && !hasAttach) {
1642
+ send({
1643
+ type: "notice",
1644
+ level: "warning",
1645
+ text: "发送已忽略:提示词为空且未附带文件或上下文引用。",
1646
+ textEn: "Prompt ignored: text is empty and no attachments were provided.",
1647
+ });
1648
+ break;
1649
+ }
1645
1650
  void cs.prompt(msg.text, msg.attachments, msg.queue);
1646
1651
  break;
1652
+ }
1647
1653
  case "queue_remove":
1648
1654
  cs.removeQueued(msg.kind, msg.text, msg.index);
1649
1655
  break;
@@ -1705,6 +1711,9 @@ wss.on("connection", (ws) => {
1705
1711
  send({ type: "tool_info", name: msg.name, found: false, unsupported: true });
1706
1712
  }
1707
1713
  break;
1714
+ case "get_compacted_messages":
1715
+ void cs.getCompactedMessages?.(msg.compactionMessageId, msg.conversationId);
1716
+ break;
1708
1717
  case "list_sessions":
1709
1718
  void cs.refreshSessions();
1710
1719
  break;
@@ -1731,6 +1740,11 @@ wss.on("connection", (ws) => {
1731
1740
  void cs.persistConversation(msg.id);
1732
1741
  }
1733
1742
  break;
1743
+ case "pin_conversation":
1744
+ if (typeof cs.setConversationPinned === "function") {
1745
+ void cs.setConversationPinned(msg.id, msg.pinned);
1746
+ }
1747
+ break;
1734
1748
  case "dismiss_finished_subagents":
1735
1749
  void cs.dismissFinishedSubagents(msg.parentId);
1736
1750
  break;
@@ -2027,6 +2041,7 @@ wss.on("connection", (ws) => {
2027
2041
  reviewModel: msg.reviewModel,
2028
2042
  maxRounds: msg.maxRounds,
2029
2043
  locked: msg.locked,
2044
+ execModel: msg.execModel,
2030
2045
  });
2031
2046
  break;
2032
2047
  case "clear_goal":
@@ -2044,6 +2059,7 @@ wss.on("connection", (ws) => {
2044
2059
  reviewModel: msg.reviewModel,
2045
2060
  maxRounds: msg.maxRounds,
2046
2061
  locked: msg.locked,
2062
+ execModel: msg.execModel,
2047
2063
  });
2048
2064
  break;
2049
2065
  case "get_settings":
@@ -2174,15 +2190,35 @@ wss.on("connection", (ws) => {
2174
2190
  // plugin_job;拒绝/超时直接回一条 done,让面板上的作业就地结束(不占
2175
2191
  // 安装锁、不弹「失败」之外的噪音)。卸载不在本门范围内(由面板本身发起)。
2176
2192
  if (jobAction === "install" || jobAction === "update") {
2177
- void confirmPluginInstall([{ id: pluginId, source: String(msg.source ?? "") }])
2193
+ const alreadyGranted = pluginMgr.permGrants.has("plugin-installer", "net", { host: "github.com" });
2194
+ if (!alreadyGranted) {
2195
+ send({
2196
+ type: "plugin_job",
2197
+ jobId,
2198
+ action: jobAction,
2199
+ pluginId,
2200
+ phase: "start",
2201
+ });
2202
+ send({
2203
+ type: "plugin_job",
2204
+ jobId,
2205
+ action: jobAction,
2206
+ pluginId,
2207
+ phase: "log",
2208
+ line: jobLang() === "zh" ? "等待确认安装授权…" : "Waiting for install confirmation…",
2209
+ });
2210
+ }
2211
+ void confirmPluginInstallHelper([{ id: pluginId, source: String(msg.source ?? "") }])
2178
2212
  .then((confirmed) => {
2179
2213
  if (!confirmed) {
2180
- jobDone(false, "用户未确认安装(拒绝或 120 秒超时)");
2214
+ jobDone(false, jobLang() === "zh"
2215
+ ? "用户未确认安装(拒绝或 120 秒超时)"
2216
+ : "Installation not confirmed (rejected or timed out after 120s)");
2181
2217
  return;
2182
2218
  }
2183
2219
  startPluginJob();
2184
2220
  })
2185
- .catch(() => jobDone(false, "安装确认流程异常"));
2221
+ .catch(() => jobDone(false, jobLang() === "zh" ? "安装确认流程异常" : "Installation confirmation error"));
2186
2222
  break;
2187
2223
  }
2188
2224
  startPluginJob();
@@ -2195,6 +2231,19 @@ wss.on("connection", (ws) => {
2195
2231
  done: async (ok, info) => {
2196
2232
  if (ok) {
2197
2233
  await reloadPluginsAndPush(jobLang);
2234
+ const isZh = jobLang() === "zh";
2235
+ const actionLabel = jobAction === "uninstall"
2236
+ ? isZh
2237
+ ? "卸载"
2238
+ : "uninstalled"
2239
+ : jobAction === "update"
2240
+ ? isZh
2241
+ ? "更新"
2242
+ : "updated"
2243
+ : isZh
2244
+ ? "安装"
2245
+ : "installed";
2246
+ cs?.emitNotice("info", `插件「${pluginId}」${actionLabel}完成`, `Plugin "${pluginId}" ${actionLabel} successfully`);
2198
2247
  }
2199
2248
  else if (info.error) {
2200
2249
  cs?.emitNotice("error", `插件操作失败:${info.error}`, `Plugin operation failed: ${info.error}`);
@@ -2354,7 +2403,7 @@ wss.on("connection", (ws) => {
2354
2403
  // 本地文件来源只允许工作区内(防任意路径文件探测 oracle)。
2355
2404
  workspaceRoot: cs?.cwd ?? CWD,
2356
2405
  // 安装确认门(P0):拒绝/超时只写目录不安装。
2357
- confirmInstall: (items) => confirmPluginInstall(items),
2406
+ confirmInstall: (items) => confirmPluginInstallHelper(items),
2358
2407
  }).then((r) => {
2359
2408
  if (r.installRefused) {
2360
2409
  cs?.emitNotice("warning", "目录已同步,但安装未获用户确认(拒绝或超时),未安装任何插件", "Catalog synced, but installation was not confirmed (denied or timed out) — nothing was installed");
@@ -2423,6 +2472,16 @@ wss.on("connection", (ws) => {
2423
2472
  case "plan_update":
2424
2473
  cs.updatePlan?.(msg.steps, msg.activeStepId, msg.conversationId);
2425
2474
  break;
2475
+ case "set_plan_mode":
2476
+ // 计划模式(只规划不实施):会话级开关,热生效(提示词重建 + 工具硬闸门)。
2477
+ void cs.setPlanMode?.(msg.enabled, msg.conversationId);
2478
+ break;
2479
+ case "set_delegate_mode":
2480
+ // 审查者模式(自动委派):会话级开关,默认关。开启后本对话只审阅,
2481
+ // 用户 prompt 由服务端转给常驻落盘执行对话。DSH 引擎的实现在
2482
+ // setDelegateMode 里直接拒(无 customTools 注册面 → 闸门无处可挂)。
2483
+ void cs.setDelegateMode?.(msg.enabled, msg.conversationId);
2484
+ break;
2426
2485
  case "page_response":
2427
2486
  // 浏览器(page-picker 扩展经前端)对 browser_page 的回包:恢复挂起的
2428
2487
  // pageCall;id 不匹配(超时后迟到/页面刷新)由 resolvePageCall 静默忽略。
@@ -795,6 +795,7 @@ export function makeLspTool(options) {
795
795
  const cwd = options.cwd;
796
796
  return defineTool({
797
797
  name: LSP_TOOL_NAME,
798
+ promptSnippet: "IDE-grade semantic analysis (LSP) across the workspace: definition, references, hover, diagnostics, symbols, impact check",
798
799
  label: "LSP code intelligence",
799
800
  description: `IDE-grade semantic analysis (LSP) across the workspace. Actions:
800
801
  - \`definition\`: definition of the symbol at \`line\`/\`character\` in \`path\` (file, line, snippet).
@@ -807,16 +808,18 @@ export function makeLspTool(options) {
807
808
  - \`cascade\`: impact check for \`path\` — report diagnostics of files referencing it.
808
809
  Lines are 1-indexed.`,
809
810
  parameters: Type.Object({
810
- action: Type.Union([
811
- Type.Literal("definition"),
812
- Type.Literal("references"),
813
- Type.Literal("hover"),
814
- Type.Literal("diagnostics"),
815
- Type.Literal("documentSymbol"),
816
- Type.Literal("read_symbol"),
817
- Type.Literal("workspaceSymbol"),
818
- Type.Literal("cascade"),
819
- ], {
811
+ action: Type.Unsafe({
812
+ type: "string",
813
+ enum: [
814
+ "definition",
815
+ "references",
816
+ "hover",
817
+ "diagnostics",
818
+ "documentSymbol",
819
+ "read_symbol",
820
+ "workspaceSymbol",
821
+ "cascade",
822
+ ],
820
823
  description: "The LSP operation to perform.",
821
824
  }),
822
825
  path: Type.Optional(Type.String({
@@ -40,14 +40,12 @@ export function describeTodos(state, includeDeleted = false, lang = "en") {
40
40
  .join("\n");
41
41
  }
42
42
  const TODO_GUIDANCE_ZH = [
43
- "# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
44
43
  "- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
45
44
  "- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
46
45
  "- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
47
46
  "- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
48
47
  ];
49
48
  const TODO_GUIDANCE_EN = [
50
- "# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
51
49
  "- Marker syntax: [[todo:new:<subject>]] to create; [[todo:set:<id>,completed|in_progress|pending]] for status; [[todo:remove:<id>]] to delete; [[todo:dep:<id>,blocks=<dep ids, comma-separated>]] to set dependencies.",
52
50
  "- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
53
51
  "- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
@@ -393,12 +393,25 @@ function adaptMcpTool(serverName, mcpTool, client) {
393
393
  name,
394
394
  label: `${serverName} · ${mcpTool.name}`,
395
395
  description: mcpTool.description ?? `Tool ${mcpTool.name} provided by MCP server "${serverName}"`,
396
+ // 无 snippet 的工具不会进系统提示词的「Available tools」清单(SDK 只列有
397
+ // snippet 的工具),MCP 工具会变成 schema-only:模型读清单以为自己没有
398
+ // 这个能力(#403)。这里从 description 提取首句做极简 snippet。
399
+ promptSnippet: firstSentenceSnippet(mcpTool.description) ?? `MCP tool ${mcpTool.name} (server: ${serverName})`,
396
400
  parameters: mcpTool.inputSchema ?? {},
397
401
  execute: async (_toolCallId, params, _signal) => {
398
402
  return client.call(mcpTool.name, params ?? {});
399
403
  },
400
404
  };
401
405
  }
406
+ /** 从 MCP 工具 description 提取首句做 promptSnippet(≤100 字符);空描述返回 undefined。 */
407
+ function firstSentenceSnippet(description) {
408
+ const text = (description ?? "").replace(/\s+/g, " ").trim();
409
+ if (!text)
410
+ return undefined;
411
+ const cut = text.search(/[.。!?!?]/);
412
+ const head = cut > 0 ? text.slice(0, cut + 1) : text;
413
+ return head.length > 100 ? `${head.slice(0, 99)}…` : head;
414
+ }
402
415
  /** 工具名必须是 [A-Za-z0-9_-]+(与插件工具同规则),MCP 可能含冒号/斜杠 — 归一化。 */
403
416
  function sanitizeToolName(name) {
404
417
  const cleaned = (name || "").replace(/[^A-Za-z0-9_-]/g, "_");
@@ -18,6 +18,7 @@ export function makePatchTool(options) {
18
18
  const cwd = options.cwd;
19
19
  return defineTool({
20
20
  name: PATCH_TOOL_NAME,
21
+ promptSnippet: "apply content-hashed, line-anchored patches to workspace files (prevents stale edits, line drift, and indentation hallucination)",
21
22
  label: "Apply Hashline patch",
22
23
  description: `Apply content-hashed, line-anchored patches to workspace files — prevents stale edits, line drift, and indentation hallucination.
23
24
  Each file section starts with \`[path#TAG]\` (or \`[path]\` if the hash is unknown yet). Operations:
@@ -0,0 +1,77 @@
1
+ /**
2
+ * turn_end boundary error patch for @earendil-works/pi-coding-agent (issue #411).
3
+ *
4
+ * In SDK 0.87+, when an in-flight turn is aborted or torn down before its assistant
5
+ * message is persisted, `_dispatchTurnEndBoundary()` in `agent-session.js` calls
6
+ * `this._extensionRunner.emitError({ event: "turn_end", error: "turn_end could not resolve..." })`
7
+ * right before returning `false`, causing false-positive extension errors to flood
8
+ * both the main conversation and subagents.
9
+ *
10
+ * This module idempotently patches `agent-session.js` in the installed SDK to
11
+ * silently skip the boundary (`return false`) without emitting an error, matching
12
+ * the upstream issue expectation (earendil-works/pi#10149).
13
+ *
14
+ * Same pattern as patch-remote-catalog.ts and patch-node-pty.ts: best-effort, idempotent,
15
+ * and skips gracefully if the SDK code structure differs or is already fixed upstream.
16
+ */
17
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { sdkCopies } from "./sdk-origin.js";
21
+ export const TARGET_PATTERN = /if\s*\(!messageEntryId\)\s*\{\s*this\._extensionRunner\.emitError\(\{\s*extensionPath:\s*"<boundary>",\s*event:\s*"turn_end",\s*error:\s*"turn_end could not resolve the persisted assistant entry ID",\s*\}\);\s*return false;\s*\}/;
22
+ export const PATCHED_SENTINEL = "// pi-web-ui patch (issue #411: silent turn_end on aborted turns)";
23
+ export const REPLACEMENT = `if (!messageEntryId) {
24
+ ${PATCHED_SENTINEL}
25
+ return false;
26
+ }`;
27
+ /**
28
+ * 纯函数:给定 agent-session.js 内容,返回打补丁后的内容。
29
+ * - 命中目标代码段 → 替换并返回新内容;
30
+ * - 已打过补丁(含 PATCHED_SENTINEL)→ 原样返回;
31
+ * - 目标代码段未命中(上游已修复或版本不匹配)→ 原样返回,静默跳过。
32
+ */
33
+ export function patchAgentSessionSource(src) {
34
+ if (src.includes(PATCHED_SENTINEL)) {
35
+ return { patched: false, code: src };
36
+ }
37
+ if (!TARGET_PATTERN.test(src)) {
38
+ return { patched: false, code: src };
39
+ }
40
+ return { patched: true, code: src.replace(TARGET_PATTERN, REPLACEMENT) };
41
+ }
42
+ export function candidateAgentSessionFiles() {
43
+ const files = new Set();
44
+ try {
45
+ const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
46
+ files.add(join(dirname(entry), "core", "agent-session.js"));
47
+ }
48
+ catch {
49
+ // Ignore resolve failures
50
+ }
51
+ try {
52
+ for (const copy of sdkCopies()) {
53
+ files.add(join(dirname(copy.path), "dist", "core", "agent-session.js"));
54
+ }
55
+ }
56
+ catch {
57
+ // Ignore copy inspection failures
58
+ }
59
+ return [...files];
60
+ }
61
+ export function applyPatch() {
62
+ for (const file of candidateAgentSessionFiles()) {
63
+ try {
64
+ if (!existsSync(file))
65
+ continue;
66
+ const src = readFileSync(file, "utf8");
67
+ const { patched, code } = patchAgentSessionSource(src);
68
+ if (patched) {
69
+ writeFileSync(file, code, "utf8");
70
+ }
71
+ }
72
+ catch {
73
+ // best-effort
74
+ }
75
+ }
76
+ }
77
+ applyPatch();
@@ -1,6 +1,50 @@
1
1
  /**
2
2
  * 权限预设相关辅助逻辑与会话回放。
3
3
  */
4
+ /**
5
+ * 从会话 sessionManager entries 回放恢复最后设定的**计划模式**状态(同 permission/preset
6
+ * 的落盘口径:切会话/重载不丢)。从未设定或非布尔 → false。
7
+ */
8
+ export function readPlanModeFromSession(sm) {
9
+ try {
10
+ const mgr = sm;
11
+ if (typeof mgr?.getEntries !== "function")
12
+ return false;
13
+ const entries = mgr.getEntries();
14
+ for (let i = entries.length - 1; i >= 0; i--) {
15
+ const e = entries[i];
16
+ if (e?.customType === "plan/mode" && typeof e.data?.enabled === "boolean")
17
+ return e.data.enabled;
18
+ }
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ return false;
24
+ }
25
+ /**
26
+ * 从会话 sessionManager entries 回放恢复最后设定的**审查者模式**状态(同 plan/mode
27
+ * 口径:customType "delegate/mode")。从未设定或非布尔 → false。
28
+ * ⚠️ 执行对话 id **不回放**:常驻执行对话是本进程内的活对象,重启后
29
+ * 首条请求会重新建一个(宁可多一个执行对话,也别指向一个不存在的 id)。
30
+ */
31
+ export function readDelegateModeFromSession(sm) {
32
+ try {
33
+ const mgr = sm;
34
+ if (typeof mgr?.getEntries !== "function")
35
+ return false;
36
+ const entries = mgr.getEntries();
37
+ for (let i = entries.length - 1; i >= 0; i--) {
38
+ const e = entries[i];
39
+ if (e?.customType === "delegate/mode" && typeof e.data?.enabled === "boolean")
40
+ return e.data.enabled;
41
+ }
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ return false;
47
+ }
4
48
  /**
5
49
  * 从会话 sessionManager entries 回放恢复最后设定的权限预设(若从未设定则返回 undefined)。
6
50
  * customType 为 "permission/preset",载荷结构为 { preset: string }。