psyclaw 0.30.15 → 0.30.17

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.
@@ -2573,6 +2573,7 @@ function groupFiles(files) {
2573
2573
  "核心文档": ordered.filter(path => priority.includes(path)),
2574
2574
  "Export 导出结果": ordered.filter(path => path.startsWith("outputs/")),
2575
2575
  "论文与报告": ordered.filter(path => /^(paper|docs)\//.test(path) && !priority.includes(path)),
2576
+ "分析报告与核查": ordered.filter(path => path.startsWith("analysis/results/")),
2576
2577
  "研究记录": ordered.filter(path => /^(notes|logs)\//.test(path) && !priority.includes(path))
2577
2578
  };
2578
2579
  }
@@ -2672,14 +2673,20 @@ async function loadEcosystem() {
2672
2673
  json("/api/recommended-agents"),
2673
2674
  json("/api/enabled-capabilities")
2674
2675
  ]);
2675
- const skillSet = new Set((enabled.skills || []).map(item => item.id));
2676
- const mcpSet = new Set((enabled.mcp || []).map(item => item.id));
2676
+ const skillStatus = new Map((enabled.skills || []).map(item => [item.id, item]));
2677
+ const mcpStatus = new Map((enabled.mcp || []).map(item => [item.id, item]));
2677
2678
  state.eco = [
2678
2679
  ...(enabled.coreSkills || []).map(item => ({ ...item, kind: "core", description: item.description || "PsyClaw 内置科研核心 Skill", sourceRef: "bundled", enabled: item.enabled, installed: true })),
2679
- ...(skills.items || []).map(item => ({ ...item, kind: "skill", enabled: skillSet.has(item.id), installed: skillSet.has(item.id) })),
2680
+ ...(skills.items || []).map(item => {
2681
+ const status = skillStatus.get(item.id) || {};
2682
+ return { ...item, kind: "skill", enabled: Boolean(status.enabled), installed: Boolean(status.installed) };
2683
+ }),
2680
2684
  ...(plugins.items || []).map(item => ({ ...item, kind: "plugin", enabled: Boolean(item.enabled), installed: Boolean(item.installed) })),
2681
2685
  ...(enabled.localSkills || []).map(item => ({ ...item, kind: "local", sourceRef: item.path || "用户配置目录", installed: true })),
2682
- ...(mcps.items || []).map(item => ({ ...item, kind: "mcp", enabled: mcpSet.has(item.id), installed: mcpSet.has(item.id) })),
2686
+ ...(mcps.items || []).map(item => {
2687
+ const status = mcpStatus.get(item.id) || {};
2688
+ return { ...item, kind: "mcp", enabled: Boolean(status.enabled), installed: Boolean(status.installed) };
2689
+ }),
2683
2690
  ...(agents.items || []).map(item => ({ ...item, kind: "agent", enabled: Boolean(item.enabled), installed: Boolean(item.installed), locked: Boolean(item.locked) })),
2684
2691
  ...(skills.externalTools || []).map(item => ({ ...item, kind: "external", enabled: false, installed: false }))
2685
2692
  ];
@@ -2723,7 +2730,13 @@ function renderEcosystem() {
2723
2730
  $("ecosystem-body").innerHTML = items.map(item => {
2724
2731
  const prereqs = itemPrereqs(item);
2725
2732
  const busy = Boolean(state.installing[item.id]);
2726
- const installLabel = busy ? "正在安装…" : item.kind === "plugin" && item.installed ? "重新安装" : item.kind === "core" ? "启用" : "安装";
2733
+ const statusLabel = busy ? "正在安装…"
2734
+ : item.locked ? "内置可用"
2735
+ : item.enabled ? (item.kind === "plugin" ? "已安装" : "已启用")
2736
+ : item.installed ? "已安装未启用"
2737
+ : item.kind === "plugin" && item.configured ? "配置存在,文件缺失"
2738
+ : item.kind === "external" ? "可安装外部工具"
2739
+ : "未安装";
2727
2740
  return `
2728
2741
  <tr>
2729
2742
  <td>
@@ -2746,25 +2759,46 @@ function renderEcosystem() {
2746
2759
  </span>
2747
2760
  </td>
2748
2761
  <td>
2749
- <span class="tag ${item.enabled ? "ok" : item.configured ? "warn" : ""}">
2750
- ${busy ? "正在安装…" : item.locked ? "内置可用" : item.enabled ? (item.kind === "plugin" ? "已安装" : "已启用") : item.kind === "plugin" && item.configured ? "配置存在,文件缺失" : item.kind === "external" ? "可安装外部工具" : "未启用"}
2762
+ <span class="tag ${item.enabled ? "ok" : item.installed || item.configured ? "warn" : ""}">
2763
+ ${esc(statusLabel)}
2751
2764
  </span>
2752
2765
  </td>
2753
2766
  <td style="text-align:right">
2754
- ${item.locked || item.kind === "agent" ? "—" :
2755
- item.kind === "plugin" && item.local ? "—" :
2756
- item.kind === "plugin" ? `<button class="button small ${item.installed ? "" : "primary"}" ${busy ? "disabled" : ""} onclick="installCapability('plugin','${esc(item.id)}')">${installLabel}</button>` :
2757
- item.kind === "external" ? `<button class="button small primary" ${busy ? "disabled" : ""} onclick="installCapability('external','${esc(item.id)}')">${installLabel}</button>` :
2758
- item.enabled ? `<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>` :
2759
- item.kind === "local" ? `<button class="button small primary" onclick="toggleCapability('local','${esc(item.id)}',true)">启用</button>` :
2760
- `<button class="button small primary" ${busy ? "disabled" : ""} onclick="installCapability('${item.kind}','${esc(item.id)}')">${installLabel}</button>`
2761
- }
2767
+ ${capabilityActionButtons(item, busy)}
2762
2768
  </td>
2763
2769
  </tr>
2764
2770
  `;
2765
2771
  }).join("") || '<tr><td colspan="5" class="empty" style="min-height:100px">未找到匹配的能力项</td></tr>';
2766
2772
  }
2767
2773
 
2774
+ function capabilityActionButtons(item, busy) {
2775
+ if (item.locked || item.kind === "agent") return "—";
2776
+ if (item.kind === "plugin" && item.local) return "—";
2777
+ if (item.kind === "plugin") {
2778
+ const label = busy ? "正在安装…" : item.installed ? "重新安装" : "安装";
2779
+ return `<button class="button small ${item.installed ? "" : "primary"}" ${busy ? "disabled" : ""} onclick="installCapability('plugin','${esc(item.id)}')">${label}</button>`;
2780
+ }
2781
+ if (item.kind === "external") {
2782
+ return `<button class="button small primary" ${busy ? "disabled" : ""} onclick="installCapability('external','${esc(item.id)}')">${busy ? "正在安装…" : "安装"}</button>`;
2783
+ }
2784
+ if (item.kind === "core" || item.kind === "local") {
2785
+ return item.enabled
2786
+ ? `<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>`
2787
+ : `<button class="button small primary" onclick="toggleCapability('${item.kind}','${esc(item.id)}',true)">启用</button>`;
2788
+ }
2789
+ // Recommended Skill / MCP: keep enable/disable separate from install/reinstall.
2790
+ const buttons = [];
2791
+ if (item.installed) {
2792
+ buttons.push(item.enabled
2793
+ ? `<button class="button small" onclick="toggleCapability('${item.kind}','${esc(item.id)}',false)">停用</button>`
2794
+ : `<button class="button small primary" onclick="toggleCapability('${item.kind}','${esc(item.id)}',true)">启用</button>`);
2795
+ buttons.push(`<button class="button small" ${busy ? "disabled" : ""} onclick="installCapability('${item.kind}','${esc(item.id)}')">${busy ? "正在安装…" : "重新安装"}</button>`);
2796
+ } else {
2797
+ buttons.push(`<button class="button small primary" ${busy ? "disabled" : ""} onclick="installCapability('${item.kind}','${esc(item.id)}')">${busy ? "正在安装…" : "安装"}</button>`);
2798
+ }
2799
+ return buttons.join(" ");
2800
+ }
2801
+
2768
2802
  async function toggleCapability(kind, id, enabled) {
2769
2803
  await json("/api/recommendation-state", {
2770
2804
  method: "POST",
@@ -2789,14 +2823,15 @@ async function installCapability(kind, id) {
2789
2823
  const scope = kind === "skill" || kind === "plugin"
2790
2824
  ? (confirm("安装到系统全局还是当前项目?\n确定:全局所有项目可用;取消:仅当前项目。") ? "user" : "project")
2791
2825
  : undefined;
2792
- let confirmText = `确认安装「${item?.name || id}」?`;
2826
+ const reinstall = Boolean(item?.installed);
2827
+ let confirmText = `确认${reinstall ? "重新安装" : "安装"}「${item?.name || id}」?`;
2793
2828
  if (kind === "external") {
2794
2829
  confirmText += "\n\n系统将按官方可信来源安装;任务会交给当前 PsyClaw 对话中的模型执行。请保持终端会话打开。";
2795
2830
  } else if (kind === "mcp" || kind === "skill") {
2796
2831
  if (prereqs.length) {
2797
2832
  confirmText += `\n\n前置依赖也将一并排队安装:${prereqs.join("、")}。\n安装任务会交给当前 PsyClaw 对话中的模型执行;请保持终端会话打开,完成后执行 /reload。`;
2798
2833
  } else {
2799
- confirmText += "\n\n安装任务会交给当前 PsyClaw 对话中的模型执行;请保持终端会话打开,完成后执行 /reload。";
2834
+ confirmText += `\n\n${reinstall ? "将覆盖当前安装目录并重新交给模型安装" : "安装任务会交给当前 PsyClaw 对话中的模型执行"};请保持终端会话打开,完成后执行 /reload。`;
2800
2835
  }
2801
2836
  } else {
2802
2837
  confirmText += "\n\n安装完成后需要执行 /reload 生效。";
@@ -36,7 +36,7 @@ import { arsRoot } from "../../ars/pi-panel-executor.js";
36
36
  import { ArsModeEditor, isArsModeEditorText } from "../../ars/mode-editor.js";
37
37
  import { MODE_STATUS, detectChatModeMismatch, formatChatModeMismatchNotice, nextSessionMode, parseSessionMode, sessionModePrompt, } from "../../session/modes.js";
38
38
  import { continuouslyWorkWarningText, isContinuouslyWorkEnabled, } from "../../session/continuously-work.js";
39
- import { assertHumanVerifyGate, formatVerifyChecklist, isNaturalPlanConfirm, isPlanRitualConfirm, PLAN_RITUAL_PHRASE, ensureDefaultVerifyChecklist, loadVerifyChecklist } from "../../verify/checklist.js";
39
+ import { assertHumanVerifyGate, formatVerifyChecklist, isNaturalPlanConfirm, isPlanRitualConfirm, PLAN_RITUAL_PHRASE, ensureDefaultVerifyChecklist, loadVerifyChecklist, recordHumanVerification } from "../../verify/checklist.js";
40
40
  import { formatSessionHelp, formatSessionHelpBrief } from "../../session/help.js";
41
41
  import { openResearchWorkbench } from "../../panel/workbench.js";
42
42
  import { captureAgentError, extractPiGeneration, finishAgentSpan, initNodeObservability, lastPiGeneration, readTelemetryPreference, shutdownObservability, startAgentSpan, trackGateWaiting, trackLlmGeneration, trackSkillInstall, withAgentSpan, writeTelemetryPreference, } from "../../observability/index.js";
@@ -448,7 +448,7 @@ async function skillManagerRows(root, state) {
448
448
  scope,
449
449
  blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
450
450
  reason: onDisk
451
- ? `已检测到目录但未通过来源/许可/哈希校验${state.skills.includes(id) ? "(当前配置为启用,但校验失败不会加载)" : ""};可按 Enter 重新安装修复`
451
+ ? `已检测到目录但未通过来源/许可/哈希校验${state.skills.includes(id) ? "(当前配置为启用,但校验失败不会加载)" : ""};可按 r 重新安装修复`
452
452
  : typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
453
453
  ? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
454
454
  : "没有可交给模型检查的来源网址",
@@ -599,7 +599,7 @@ async function openMcpManager(ctx, rows) {
599
599
  return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
600
600
  title: "MCP 管理",
601
601
  itemLabel: "MCP",
602
- footer: "↑/↓ 移动 · Space/Enter 安装或启停 · a 全部开启 · d 全部关闭 · Esc 关闭",
602
+ footer: "↑/↓ 移动 · Space/Enter 安装或启停 · r 重新安装 · a 全部开启 · d 全部关闭 · Esc 关闭",
603
603
  enabledText: "已开启;再次点击可关闭",
604
604
  disabledText: "已安装,当前未启用;点击可开启",
605
605
  missingMessage: "尚未安装;点击交给模型安装并默认启用",
@@ -1330,6 +1330,25 @@ export default function psyclawExtension(pi) {
1330
1330
  }
1331
1331
  const mode = arsModeEditor?.getMode() ?? sessionMode;
1332
1332
  const trimmed = event.text.trim();
1333
+ // A direct user statement is a valid human-review route when Panel is unavailable.
1334
+ // Do not infer approval from assistant output, quoted text, or a question about verification.
1335
+ if (/^我(?:已核验|已核实|已审阅并确认|核验了)(?:分析报告|论文定稿)?[。!!]?$/u.test(trimmed)) {
1336
+ const scope = trimmed.includes("论文定稿") ? "academic-finalize"
1337
+ : trimmed.includes("分析报告") ? "analysis-complete"
1338
+ : mode === "academic" ? "academic-finalize" : mode === "analysis" ? "analysis-complete" : undefined;
1339
+ if (!scope || !(await readActiveProject(ctx.cwd))) {
1340
+ ctx.ui.notify("请先进入已初始化项目的 analysis 或 academic 模式,或明确说「我已核验分析报告」/「我已核验论文定稿」。", "warning");
1341
+ return { action: "handled" };
1342
+ }
1343
+ try {
1344
+ await recordHumanVerification(ctx.cwd, scope, trimmed);
1345
+ ctx.ui.notify(`已记录用户人审确认(${scope});不会自动将分析标为完成或交接。`, "info");
1346
+ }
1347
+ catch (error) {
1348
+ ctx.ui.notify(`人审记录失败:${error instanceof Error ? error.message : String(error)}`, "error");
1349
+ }
1350
+ return { action: "handled" };
1351
+ }
1333
1352
  // Chat must not soft-takeover; when intent fits analysis/academic, remind only.
1334
1353
  if (mode === "chat") {
1335
1354
  const mismatch = detectChatModeMismatch(trimmed);