psyclaw 0.27.6 → 0.27.12
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.
- package/README.md +17 -14
- package/dist/apps/panel/index.html +11 -11
- package/dist/src/adapters/pi/extension.js +593 -101
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/agents/import.js +2 -1
- package/dist/src/agents/import.js.map +1 -1
- package/dist/src/analysis/hooks.d.ts +19 -2
- package/dist/src/analysis/hooks.js +48 -7
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/branding.d.ts +2 -2
- package/dist/src/branding.js +2 -2
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.js +5 -2
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +21 -6
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/installer.js +3 -0
- package/dist/src/install/installer.js.map +1 -1
- package/dist/src/integrations/mcp-runtime.d.ts +13 -0
- package/dist/src/integrations/mcp-runtime.js +48 -0
- package/dist/src/integrations/mcp-runtime.js.map +1 -1
- package/dist/src/orchestration/runner.js +1 -0
- package/dist/src/orchestration/runner.js.map +1 -1
- package/dist/src/panel/events.d.ts +2 -1
- package/dist/src/panel/events.js +2 -5
- package/dist/src/panel/events.js.map +1 -1
- package/dist/src/panel/server.js +44 -18
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/approvals.d.ts +50 -0
- package/dist/src/project/approvals.js +62 -0
- package/dist/src/project/approvals.js.map +1 -0
- package/dist/src/project/bootstrap.d.ts +17 -0
- package/dist/src/project/bootstrap.js +87 -1
- package/dist/src/project/bootstrap.js.map +1 -1
- package/dist/src/project/paths.js +5 -2
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/research/ledger.d.ts +3 -2
- package/dist/src/research/ledger.js +47 -2
- package/dist/src/research/ledger.js.map +1 -1
- package/dist/src/skills/user-skills.d.ts +65 -0
- package/dist/src/skills/user-skills.js +340 -0
- package/dist/src/skills/user-skills.js.map +1 -0
- package/dist/src/style/cli-ui.js +19 -30
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +8 -6
- package/dist/src/telemetry/export.js +81 -18
- package/dist/src/telemetry/export.js.map +1 -1
- package/dist/src/tui/provider-picker.d.ts +2 -0
- package/dist/src/tui/provider-picker.js +10 -0
- package/dist/src/tui/provider-picker.js.map +1 -1
- package/dist/src/tui/skill-manager.d.ts +3 -0
- package/dist/src/tui/skill-manager.js +11 -1
- package/dist/src/tui/skill-manager.js.map +1 -1
- package/dist/src/updates/update.js +10 -17
- package/dist/src/updates/update.js.map +1 -1
- package/dist/src/wizard.js +22 -8
- package/dist/src/wizard.js.map +1 -1
- package/dist/src/workflows/analysis-delegation.js +2 -2
- package/dist/src/workflows/analysis-delegation.js.map +1 -1
- package/package.json +2 -2
- package/scripts/rebrand-pi.mjs +94 -4
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { asProject, bootstrapProject, exportTraces, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
|
|
3
|
+
import { appendApproval, approvalInputDigest, approvePrimaryDocument, asProject, bootstrapProject, DEFAULT_RUN_APPROVAL_NODES, exportTraces, PRIMARY_PLAN_DOCUMENTS, primaryPlanApprovalStatus, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, exportAcademicDocument, recordCitationUse, runParallelPeerReview, writeHandoff } from "../../index.js";
|
|
4
4
|
import { runPlanWithPi } from "../../orchestration/pi-executor.js";
|
|
5
5
|
import { atomicWriteFile } from "../../project/jsonl.js";
|
|
6
6
|
import { RunEventLog } from "../../panel/events.js";
|
|
7
7
|
import { readProject } from "../../research/ledger.js";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { mkdir, readFile } from "node:fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
9
|
+
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
10
|
+
import { PROVIDER_PRESETS, providerCredentialSource, saveProviderConfig } from "../../setup.js";
|
|
12
11
|
import { dirname } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
13
|
import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
15
14
|
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, installLocalSkill, userSkillId, } from "../../skills/user-skills.js";
|
|
16
|
+
import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
|
|
17
|
+
import { SecretInputComponent, } from "../../tui/provider-picker.js";
|
|
18
18
|
const PARADIGMS = new Set([
|
|
19
19
|
"survey-observational",
|
|
20
20
|
"qualitative-thematic",
|
|
@@ -47,7 +47,17 @@ function parseInitArgs(args) {
|
|
|
47
47
|
async function notifyError(ctx, error) {
|
|
48
48
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
49
49
|
}
|
|
50
|
+
async function runPluginCommand(args) {
|
|
51
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
52
|
+
const modulePath = join(dirname(entry), "package-manager-cli.js");
|
|
53
|
+
const { handlePackageCommand } = await import(pathToFileURL(modulePath).href);
|
|
54
|
+
const handled = await handlePackageCommand(args);
|
|
55
|
+
if (!handled)
|
|
56
|
+
throw new Error(`不支持的 Plugin 操作:${args[0] ?? ""}`);
|
|
57
|
+
}
|
|
50
58
|
const activeAgentRuns = new Set();
|
|
59
|
+
const pendingInitApprovals = new Set();
|
|
60
|
+
const activeApprovalDialogs = new Set();
|
|
51
61
|
const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
52
62
|
function coreSkillPath(name) {
|
|
53
63
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
@@ -110,8 +120,10 @@ async function saveDefaultModel(provider, model) {
|
|
|
110
120
|
await atomicWriteFile(path, `${JSON.stringify({ ...settings, defaultProvider: provider, defaultModel: model }, null, 2)}\n`);
|
|
111
121
|
}
|
|
112
122
|
async function pickProviderItem(ctx, title, items) {
|
|
113
|
-
const
|
|
114
|
-
|
|
123
|
+
const labels = items.map((item) => `${item.current ? "* " : ""}${item.label} (${item.id})`);
|
|
124
|
+
const selected = await ctx.ui.select(title, labels, { timeout: 60_000 });
|
|
125
|
+
const index = selected === undefined ? -1 : labels.indexOf(selected);
|
|
126
|
+
return index < 0 ? undefined : items[index].id;
|
|
115
127
|
}
|
|
116
128
|
async function promptProviderKey(ctx, providerName, envName) {
|
|
117
129
|
const result = await ctx.ui.custom((tui, theme, keybindings, done) => new SecretInputComponent(`配置 ${providerName}`, envName, tui, theme, keybindings, done));
|
|
@@ -154,7 +166,7 @@ function academicGrillRequest(subject, mode) {
|
|
|
154
166
|
"从最上游、影响最大的未决问题开始。每一轮只问一个问题,同时给出你的推荐答案或推荐决策及主要取舍。",
|
|
155
167
|
"明确区分探索性与确证性研究,不把规划内容当作已有证据。直到关键分支已经解决或明确交由研究者决定后,再整理研究规格。",
|
|
156
168
|
mode === "init"
|
|
157
|
-
? "持久化模式:init。追问完成后直接更新 academic-grill Skill
|
|
169
|
+
? "持久化模式:init。追问完成后直接更新 academic-grill Skill 规定的全部项目文档,不再询问是否更新;写入后列出实际更新的文件,并把 notes/research-spec.md 与 notes/plan.md 的 Status 统一写为 awaiting-human-approval。"
|
|
158
170
|
: "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
|
|
159
171
|
].join("\n");
|
|
160
172
|
}
|
|
@@ -165,28 +177,37 @@ async function readControlledRun(root) {
|
|
|
165
177
|
try {
|
|
166
178
|
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
167
179
|
return value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"
|
|
168
|
-
? value
|
|
180
|
+
? { ...value, runId: typeof value.runId === "string" ? value.runId : `legacy_${value.projectId}`, mode: value.mode === "auto" ? "auto" : "human", selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
169
181
|
: null;
|
|
170
182
|
}
|
|
171
183
|
catch {
|
|
172
184
|
return null;
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
|
-
async function activateControlledRun(root, projectId, objective) {
|
|
187
|
+
async function activateControlledRun(root, runId, projectId, objective, selectedSkills, mode) {
|
|
176
188
|
const state = {
|
|
177
189
|
schemaVersion: "psyclaw/controlled-run/v1",
|
|
190
|
+
runId,
|
|
178
191
|
projectId,
|
|
179
192
|
objective,
|
|
193
|
+
selectedSkills: [...new Set(selectedSkills)],
|
|
194
|
+
mode,
|
|
180
195
|
activatedAt: new Date().toISOString(),
|
|
181
196
|
status: "active",
|
|
182
197
|
};
|
|
183
198
|
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
184
199
|
return state;
|
|
185
200
|
}
|
|
186
|
-
function controlledRunRequest(objective) {
|
|
201
|
+
function controlledRunRequest(objective, selectedSkills, mode) {
|
|
187
202
|
return [
|
|
188
|
-
|
|
203
|
+
`PsyClaw ${mode === "auto" ? "自动" : "人在环路"}研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
|
|
189
204
|
`本次目标:${objective}`,
|
|
205
|
+
selectedSkills.length > 0
|
|
206
|
+
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
207
|
+
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
208
|
+
mode === "auto"
|
|
209
|
+
? "自动模式:计划内普通步骤连续推进,不再逐项询问;运行时会自动记录审批。原始数据、凭据、破坏性命令、伦理决定、未经核验的事实与外部发布仍必须停止并请求用户决定。"
|
|
210
|
+
: "人在环路模式:每个有副作用或关键方法决策的步骤都必须通过交互审批;拒绝或超时立即停止,不得绕过审批。",
|
|
190
211
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
191
212
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
192
213
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
@@ -196,6 +217,129 @@ function controlledRunRequest(objective) {
|
|
|
196
217
|
"如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
|
|
197
218
|
].join("\n");
|
|
198
219
|
}
|
|
220
|
+
function parseRunArgs(args) {
|
|
221
|
+
const value = args.trim();
|
|
222
|
+
const auto = value === "auto" || value.startsWith("auto ");
|
|
223
|
+
const rest = auto ? value.slice(4).trim() : value;
|
|
224
|
+
if (!rest.startsWith("--skills"))
|
|
225
|
+
return { objective: rest, mode: auto ? "auto" : "human" };
|
|
226
|
+
const match = rest.match(/^--skills(?:=|\s+)([^\s]+)(?:\s+([\s\S]*))?$/);
|
|
227
|
+
if (!match)
|
|
228
|
+
throw new Error("Usage: /run [auto] [--skills skill-a,skill-b] [objective]");
|
|
229
|
+
return {
|
|
230
|
+
objective: (match[2] ?? "").trim(),
|
|
231
|
+
requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
|
|
232
|
+
mode: auto ? "auto" : "human",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async function planDocumentsReady(root) {
|
|
236
|
+
try {
|
|
237
|
+
const [spec, plan] = await Promise.all([
|
|
238
|
+
readFile(join(root, "notes", "research-spec.md"), "utf8"),
|
|
239
|
+
readFile(join(root, "notes", "plan.md"), "utf8"),
|
|
240
|
+
]);
|
|
241
|
+
return /Status:\s*awaiting-human-approval/i.test(spec) && /Status:\s*awaiting-human-approval/i.test(plan);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function reviewPrimaryDocuments(ctx) {
|
|
248
|
+
if (!ctx.hasUI || activeApprovalDialogs.has(ctx.cwd))
|
|
249
|
+
return false;
|
|
250
|
+
if (!(await planDocumentsReady(ctx.cwd))) {
|
|
251
|
+
ctx.ui.notify("主要计划文档尚未生成完成;请先完成 /init 的学术追问。", "warning");
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
activeApprovalDialogs.add(ctx.cwd);
|
|
255
|
+
try {
|
|
256
|
+
for (const document of PRIMARY_PLAN_DOCUMENTS) {
|
|
257
|
+
const choice = await ctx.ui.select(`审批${document.title}:${document.path}`, ["批准当前版本", "拒绝并返回修改"], { timeout: 120_000 });
|
|
258
|
+
const approved = choice === "批准当前版本";
|
|
259
|
+
await approvePrimaryDocument(ctx.cwd, document, approved ? "approved" : "rejected");
|
|
260
|
+
if (!approved) {
|
|
261
|
+
ctx.ui.notify(`${document.title}未批准;修改后运行 /approve 重新审批。`, "warning");
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
ctx.ui.notify("主要计划文档已批准。文档内容变化后审批会自动失效。", "info");
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
activeApprovalDialogs.delete(ctx.cwd);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function approveRunNodes(ctx, runId, mode) {
|
|
273
|
+
for (const node of DEFAULT_RUN_APPROVAL_NODES) {
|
|
274
|
+
if (mode === "auto") {
|
|
275
|
+
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision: "auto-approved", actor: "auto", runId, summary: node.summary });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const options = node.required ? ["批准", "拒绝并停止"] : ["批准", "本次跳过", "拒绝并停止"];
|
|
279
|
+
const choice = await ctx.ui.select(`${node.title}\n${node.summary}`, options, { timeout: 120_000 });
|
|
280
|
+
const decision = choice === "批准" ? "approved" : choice === "本次跳过" ? "skipped" : "rejected";
|
|
281
|
+
await appendApproval(ctx.cwd, { kind: "step", nodeId: node.id, decision, actor: "human", runId, summary: node.summary });
|
|
282
|
+
if (decision === "rejected" || (node.required && decision !== "approved"))
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
function toolApprovalSummary(toolName, input) {
|
|
288
|
+
const record = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
289
|
+
const target = [record.path, record.file_path, record.command, record.action, record.server, record.tool]
|
|
290
|
+
.find((value) => typeof value === "string");
|
|
291
|
+
return `${toolName}${target ? `: ${target.slice(0, 240)}` : ""}`;
|
|
292
|
+
}
|
|
293
|
+
function toolNeedsApproval(toolName, input) {
|
|
294
|
+
if (["write", "edit", "bash", "powershell", "psyclaw_workbench", "psyclaw_mcp"].includes(toolName))
|
|
295
|
+
return true;
|
|
296
|
+
const action = input && typeof input === "object" && !Array.isArray(input) ? input.action : undefined;
|
|
297
|
+
return typeof action === "string" && /write|save|publish|install|download|execute|call/i.test(action);
|
|
298
|
+
}
|
|
299
|
+
function autoApprovalBlocked(toolName, input) {
|
|
300
|
+
const text = JSON.stringify(input ?? {}).toLocaleLowerCase();
|
|
301
|
+
if (/data[\\/]raw|credential|secret|auth\.json|api[_ -]?key/.test(text))
|
|
302
|
+
return "自动模式不能批准原始数据或凭据路径操作";
|
|
303
|
+
if (["bash", "powershell"].includes(toolName) && /\b(rm|rmdir|del|remove-item|format|shutdown|reboot|git\s+reset\s+--hard)\b/i.test(text)) {
|
|
304
|
+
return "自动模式不能批准破坏性命令";
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
async function selectableRunSkills(root) {
|
|
309
|
+
const state = await readRecommendationState(root);
|
|
310
|
+
const rows = await skillManagerRows(root, state);
|
|
311
|
+
const available = [
|
|
312
|
+
...coreSkillNames().map((name) => ({ id: name, name })),
|
|
313
|
+
...rows.filter((row) => row.installed && row.enabled && !row.blocked).map((row) => ({ id: row.id, name: row.name })),
|
|
314
|
+
];
|
|
315
|
+
const seen = new Set();
|
|
316
|
+
return available.filter((skill) => {
|
|
317
|
+
const key = skill.name.trim().toLocaleLowerCase();
|
|
318
|
+
if (!key || seen.has(key))
|
|
319
|
+
return false;
|
|
320
|
+
seen.add(key);
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function resolveRunSkills(requested, available) {
|
|
325
|
+
const lookup = new Map();
|
|
326
|
+
for (const skill of available) {
|
|
327
|
+
lookup.set(skill.id.toLocaleLowerCase(), skill.name);
|
|
328
|
+
lookup.set(skill.name.toLocaleLowerCase(), skill.name);
|
|
329
|
+
}
|
|
330
|
+
const selected = [];
|
|
331
|
+
const unknown = [];
|
|
332
|
+
for (const value of requested) {
|
|
333
|
+
const name = lookup.get(value.toLocaleLowerCase());
|
|
334
|
+
if (!name)
|
|
335
|
+
unknown.push(value);
|
|
336
|
+
else if (!selected.some((item) => item.toLocaleLowerCase() === name.toLocaleLowerCase()))
|
|
337
|
+
selected.push(name);
|
|
338
|
+
}
|
|
339
|
+
if (unknown.length > 0)
|
|
340
|
+
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skill 安装或启用。`);
|
|
341
|
+
return selected;
|
|
342
|
+
}
|
|
199
343
|
async function recommendedItems(kind) {
|
|
200
344
|
const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
|
|
201
345
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -219,11 +363,13 @@ async function recommendedItems(kind) {
|
|
|
219
363
|
return { items: [], externalTools: [], installPrep: [] };
|
|
220
364
|
}
|
|
221
365
|
function skillScopeLabel(scope) {
|
|
366
|
+
if (scope === "local")
|
|
367
|
+
return "用户本地目录(所有项目可发现)";
|
|
222
368
|
return scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)";
|
|
223
369
|
}
|
|
224
370
|
async function skillManagerRows(root, state) {
|
|
225
371
|
const catalog = await readRecommendedCatalog();
|
|
226
|
-
|
|
372
|
+
const recommendedRows = await Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
|
|
227
373
|
const id = normalizeRecommendedSkillId(item.id);
|
|
228
374
|
const scope = state.skillScopes?.[id] ?? "project";
|
|
229
375
|
try {
|
|
@@ -239,9 +385,21 @@ async function skillManagerRows(root, state) {
|
|
|
239
385
|
enabled: state.skills.includes(id),
|
|
240
386
|
scope,
|
|
241
387
|
blocked: false,
|
|
388
|
+
source: "recommended",
|
|
242
389
|
};
|
|
243
390
|
}
|
|
244
|
-
catch {
|
|
391
|
+
catch (validationError) {
|
|
392
|
+
// A managed target that exists on disk but fails validation (e.g. a URL
|
|
393
|
+
// install whose manifest/hash does not match the pinned catalog) is still
|
|
394
|
+
// shown as installed so the user sees it; enabling it only takes effect
|
|
395
|
+
// once the install is repaired through /install.
|
|
396
|
+
let onDisk = false;
|
|
397
|
+
try {
|
|
398
|
+
const target = recommendedSkillTarget(root, id, scope);
|
|
399
|
+
const stat = await lstat(target);
|
|
400
|
+
onDisk = stat.isDirectory();
|
|
401
|
+
}
|
|
402
|
+
catch { /* target missing */ }
|
|
245
403
|
return {
|
|
246
404
|
id,
|
|
247
405
|
name: item.name,
|
|
@@ -249,20 +407,50 @@ async function skillManagerRows(root, state) {
|
|
|
249
407
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
250
408
|
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
251
409
|
collection: item.skillLayout === "collection",
|
|
252
|
-
installed:
|
|
253
|
-
enabled:
|
|
410
|
+
installed: onDisk,
|
|
411
|
+
enabled: onDisk && state.skills.includes(id),
|
|
254
412
|
scope,
|
|
255
413
|
blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
|
|
256
|
-
reason:
|
|
257
|
-
? "
|
|
258
|
-
: "
|
|
414
|
+
reason: onDisk
|
|
415
|
+
? `已检测到目录但未通过来源/许可/哈希校验${state.skills.includes(id) ? "(当前配置为启用,但校验失败不会加载)" : ""};可按 Enter 重新安装修复`
|
|
416
|
+
: typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
|
|
417
|
+
? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
|
|
418
|
+
: "没有可交给模型检查的来源网址",
|
|
419
|
+
source: "recommended",
|
|
259
420
|
};
|
|
260
421
|
}
|
|
261
422
|
}));
|
|
423
|
+
// Merge user-installed local skills from the expanded discovery roots so the
|
|
424
|
+
// management page shows them and lets the user toggle each one. Managed
|
|
425
|
+
// recommended installs are excluded here — they are shown through their
|
|
426
|
+
// recommended rows above (including on-disk-but-unvalidated installs).
|
|
427
|
+
const userState = await readUserSkillState(root);
|
|
428
|
+
const disabled = new Set(userState.disabled);
|
|
429
|
+
const localSkills = await scanLocalSkills(root, { includeManaged: false });
|
|
430
|
+
const localRows = localSkills.map((skill) => ({
|
|
431
|
+
id: userSkillId(skill.name),
|
|
432
|
+
name: skill.name,
|
|
433
|
+
description: skill.description,
|
|
434
|
+
sourceRef: skill.path,
|
|
435
|
+
collection: false,
|
|
436
|
+
installed: true,
|
|
437
|
+
enabled: !disabled.has(skill.name),
|
|
438
|
+
scope: "local",
|
|
439
|
+
blocked: false,
|
|
440
|
+
source: "local",
|
|
441
|
+
duplicatePaths: skill.duplicatePaths,
|
|
442
|
+
}));
|
|
443
|
+
// A catalog skill that is actually installed is shown once through its
|
|
444
|
+
// recommended row (which carries source/trust info); purely local skills —
|
|
445
|
+
// including a local copy of a catalog skill that failed managed validation —
|
|
446
|
+
// are appended so they stay visible and toggleable.
|
|
447
|
+
const installedRecommendedNames = new Set(recommendedRows.filter((row) => row.installed).map((row) => row.name.toLocaleLowerCase()));
|
|
448
|
+
const uniqueLocalRows = localRows.filter((row) => !installedRecommendedNames.has(row.name.toLocaleLowerCase()));
|
|
449
|
+
return [...recommendedRows, ...uniqueLocalRows];
|
|
262
450
|
}
|
|
263
|
-
async function mcpManagerRows(state) {
|
|
451
|
+
async function mcpManagerRows(root, state, runtime) {
|
|
264
452
|
const { items, installPrep } = await recommendedItems("mcp");
|
|
265
|
-
|
|
453
|
+
const recommendedRows = items.map((item) => {
|
|
266
454
|
const id = String(item.id ?? "");
|
|
267
455
|
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
268
456
|
const dependencies = Array.isArray(plan?.dependencies)
|
|
@@ -274,6 +462,7 @@ async function mcpManagerRows(state) {
|
|
|
274
462
|
description: String(item.description ?? ""),
|
|
275
463
|
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
276
464
|
enabled: state.mcp.includes(id),
|
|
465
|
+
source: "recommended",
|
|
277
466
|
details: [
|
|
278
467
|
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
279
468
|
`参考版本:${String(plan?.ref ?? "由模型读取来源确定")} · 许可证:${String(plan?.license ?? "由模型读取来源确定")}`,
|
|
@@ -281,6 +470,31 @@ async function mcpManagerRows(state) {
|
|
|
281
470
|
],
|
|
282
471
|
};
|
|
283
472
|
});
|
|
473
|
+
// Merge user-configured MCP servers (`.psyclaw/mcp/*.json` and
|
|
474
|
+
// `~/.psyclaw/mcp/*.json`) so the management page shows them too, even when
|
|
475
|
+
// disabled. User rows win over a same-id recommended row.
|
|
476
|
+
const userRows = (await runtime.listUserConfigs(root)).map((entry) => ({
|
|
477
|
+
id: entry.id,
|
|
478
|
+
name: entry.name,
|
|
479
|
+
description: `用户配置:${entry.command}`,
|
|
480
|
+
enabled: entry.enabled,
|
|
481
|
+
source: "user",
|
|
482
|
+
userEntry: entry,
|
|
483
|
+
details: [
|
|
484
|
+
`传输:stdio · 范围:${entry.scope === "user" ? "用户级" : "项目级"}`,
|
|
485
|
+
`配置:${entry.path}`,
|
|
486
|
+
],
|
|
487
|
+
}));
|
|
488
|
+
const merged = new Map(recommendedRows.map((row) => [row.id, row]));
|
|
489
|
+
for (const row of userRows)
|
|
490
|
+
merged.set(row.id, row);
|
|
491
|
+
return [...merged.values()].sort((left, right) => {
|
|
492
|
+
if (left.source === "user" && right.source !== "user")
|
|
493
|
+
return -1;
|
|
494
|
+
if (right.source === "user" && left.source !== "user")
|
|
495
|
+
return 1;
|
|
496
|
+
return left.id.localeCompare(right.id);
|
|
497
|
+
});
|
|
284
498
|
}
|
|
285
499
|
function skillRowLabel(row) {
|
|
286
500
|
const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
|
|
@@ -316,7 +530,12 @@ function skillManagerItems(rows) {
|
|
|
316
530
|
description: row.description,
|
|
317
531
|
status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
|
|
318
532
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
319
|
-
details: [
|
|
533
|
+
details: [
|
|
534
|
+
`安装位置:${skillScopeLabel(row.scope)}`,
|
|
535
|
+
...(row.duplicatePaths && row.duplicatePaths.length > 0
|
|
536
|
+
? [`已忽略 ${row.duplicatePaths.length} 个同名来源`]
|
|
537
|
+
: []),
|
|
538
|
+
],
|
|
320
539
|
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
321
540
|
})),
|
|
322
541
|
];
|
|
@@ -339,18 +558,14 @@ async function openMcpManager(ctx, rows) {
|
|
|
339
558
|
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
340
559
|
title: "MCP 管理",
|
|
341
560
|
itemLabel: "MCP",
|
|
342
|
-
footer: "↑/↓ 移动 · Space
|
|
561
|
+
footer: "↑/↓ 移动 · Space 开启/关闭 · a 全部开启 · d 全部关闭 · Enter 交给模型安装并配置 · Esc 关闭",
|
|
343
562
|
enterAction: "install",
|
|
344
|
-
enabledText: "
|
|
345
|
-
disabledText: "
|
|
563
|
+
enabledText: "已开启;Enter 可让当前模型重新安装或修复推荐配置",
|
|
564
|
+
disabledText: "已关闭",
|
|
346
565
|
})));
|
|
347
566
|
}
|
|
348
567
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
349
568
|
const state = await readRecommendationState(root);
|
|
350
|
-
const rows = await mcpManagerRows(state);
|
|
351
|
-
const row = rows.find((candidate) => candidate.id === id);
|
|
352
|
-
if (!row)
|
|
353
|
-
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
354
569
|
const current = new Set(state.mcp);
|
|
355
570
|
if (enabled)
|
|
356
571
|
current.add(id);
|
|
@@ -359,6 +574,9 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
359
574
|
state.mcp = [...current];
|
|
360
575
|
await saveRecommendationState(root, state);
|
|
361
576
|
}
|
|
577
|
+
async function setUserMcpEnabled(entry, enabled) {
|
|
578
|
+
await setUserMcpConfigEnabled(entry, enabled);
|
|
579
|
+
}
|
|
362
580
|
function modelMcpInstallTask(root, row, plan) {
|
|
363
581
|
const suggestedCommand = typeof plan?.command === "string" ? plan.command : "请根据来源仓库的最新安装说明确定";
|
|
364
582
|
const suggestedTarget = typeof plan?.target === "string" ? plan.target : `.psyclaw/mcp/${row.id}.json`;
|
|
@@ -381,48 +599,86 @@ async function queueModelMcpInstall(pi, ctx, row, plan) {
|
|
|
381
599
|
pi.sendUserMessage(modelMcpInstallTask(ctx.cwd, row, plan), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
382
600
|
ctx.ui.notify(`已将 ${row.name} 的下载、安装和配置任务交给当前模型。模型完成并确认可启动后,请执行 /reload。`, "info");
|
|
383
601
|
}
|
|
384
|
-
async function showMcpManager(pi, args, ctx) {
|
|
602
|
+
async function showMcpManager(pi, args, ctx, runtime) {
|
|
385
603
|
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
386
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
387
|
-
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
|
|
604
|
+
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
605
|
+
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|enable-all|disable-all|install <id>]");
|
|
388
606
|
}
|
|
389
607
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
390
608
|
throw new Error(`Usage: /mcp ${verb} <id>`);
|
|
391
609
|
}
|
|
392
610
|
const catalog = await recommendedItems("mcp");
|
|
393
611
|
if (verb === "install") {
|
|
394
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
612
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
395
613
|
const row = rows.find((candidate) => candidate.id === id);
|
|
396
614
|
if (!row)
|
|
397
615
|
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
616
|
+
if (row.source !== "recommended")
|
|
617
|
+
throw new Error(`MCP ${id} 已是用户配置,无需再次安装`);
|
|
398
618
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
399
619
|
return;
|
|
400
620
|
}
|
|
621
|
+
if (verb === "enable-all" || verb === "disable-all") {
|
|
622
|
+
const enabled = verb === "enable-all";
|
|
623
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
624
|
+
for (const row of rows) {
|
|
625
|
+
if (row.source === "user" && row.userEntry)
|
|
626
|
+
await setUserMcpEnabled(row.userEntry, enabled);
|
|
627
|
+
else
|
|
628
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, enabled);
|
|
629
|
+
}
|
|
630
|
+
ctx.ui.notify(`已批量${enabled ? "开启" : "关闭"}全部 MCP 配置;重启后重新检查运行时可用性`, "info");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
401
633
|
if (verb === "enable" || verb === "disable") {
|
|
402
|
-
await
|
|
403
|
-
|
|
634
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
635
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
636
|
+
if (!row)
|
|
637
|
+
throw new Error(`未找到 MCP: ${id}`);
|
|
638
|
+
if (row.source === "user" && row.userEntry)
|
|
639
|
+
await setUserMcpEnabled(row.userEntry, verb === "enable");
|
|
640
|
+
else
|
|
641
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, verb === "enable");
|
|
642
|
+
ctx.ui.notify(`MCP ${id} 已${verb === "enable" ? "开启" : "关闭"};重启后重新检查安装、信任和工具策略`, "info");
|
|
404
643
|
return;
|
|
405
644
|
}
|
|
406
645
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
407
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
408
|
-
ctx.ui.notify(rows.map((row) => `${row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}`).join("\n"), "info");
|
|
646
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
647
|
+
ctx.ui.notify(rows.map((row) => `${row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.source === "user" ? "(用户配置)" : ""}`).join("\n"), "info");
|
|
409
648
|
return;
|
|
410
649
|
}
|
|
411
650
|
while (true) {
|
|
412
|
-
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
651
|
+
const rows = await mcpManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd), runtime);
|
|
413
652
|
const action = await openMcpManager(ctx, rows);
|
|
414
653
|
if (action.type === "close")
|
|
415
654
|
return;
|
|
655
|
+
if (action.type === "toggle-all") {
|
|
656
|
+
for (const row of rows) {
|
|
657
|
+
if (row.source === "user" && row.userEntry)
|
|
658
|
+
await setUserMcpEnabled(row.userEntry, action.enabled);
|
|
659
|
+
else
|
|
660
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
661
|
+
}
|
|
662
|
+
ctx.ui.notify(`已批量${action.enabled ? "开启" : "关闭"}全部 MCP 配置;重启后重新检查运行时可用性`, "info");
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
416
665
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
417
666
|
if (!row)
|
|
418
667
|
continue;
|
|
419
668
|
if (action.type === "install") {
|
|
669
|
+
if (row.source !== "recommended") {
|
|
670
|
+
ctx.ui.notify(`${row.name} 已是用户配置,无需再次安装`, "info");
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
420
673
|
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
421
674
|
return;
|
|
422
675
|
}
|
|
423
676
|
if (action.type === "toggle") {
|
|
424
|
-
|
|
425
|
-
|
|
677
|
+
if (row.source === "user" && row.userEntry)
|
|
678
|
+
await setUserMcpEnabled(row.userEntry, action.enabled);
|
|
679
|
+
else
|
|
680
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
681
|
+
ctx.ui.notify(`${row.name} 已${action.enabled ? "开启" : "关闭"};重启后重新检查运行时可用性`, "info");
|
|
426
682
|
}
|
|
427
683
|
}
|
|
428
684
|
}
|
|
@@ -434,12 +690,12 @@ function modelSkillInstallTask(root, row, scope) {
|
|
|
434
690
|
...(row.installHint ? [`仓库入口提示:${row.installHint}`] : []),
|
|
435
691
|
`安装位置:${skillScopeLabel(scope)}。`,
|
|
436
692
|
`唯一允许的最终目标目录:${target}`,
|
|
437
|
-
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
|
|
693
|
+
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 .psyclaw/data/raw、data/raw、.git 或研究产物。",
|
|
438
694
|
row.collection
|
|
439
695
|
? "这是多 Skill 套件:目标目录自身无需 SKILL.md,但其子目录必须包含一个或多个有效 SKILL.md。保留套件内共享目录和相对路径,不得包含 .git、符号链接或凭据。"
|
|
440
696
|
: "目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
|
|
441
697
|
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
442
|
-
"安装完成后检查目标目录结构,并提醒用户执行 /
|
|
698
|
+
"安装完成后检查目标目录结构,并提醒用户执行 /skill 启用该项,再执行 /reload。",
|
|
443
699
|
].join("\n");
|
|
444
700
|
}
|
|
445
701
|
async function chooseSkillScope(ctx) {
|
|
@@ -465,17 +721,17 @@ async function queueModelSkillInstall(pi, ctx, row) {
|
|
|
465
721
|
state.skillScopes = { ...(state.skillScopes ?? {}), [row.id]: scope };
|
|
466
722
|
await saveRecommendationState(ctx.cwd, state);
|
|
467
723
|
pi.sendUserMessage(modelSkillInstallTask(ctx.cwd, row, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
468
|
-
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /
|
|
724
|
+
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /skill 中启用,再执行 /reload。`, "info");
|
|
469
725
|
}
|
|
470
726
|
async function showSkillManager(pi, args, ctx) {
|
|
471
727
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
472
728
|
const verb = action[0];
|
|
473
729
|
const id = action[1];
|
|
474
|
-
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
475
|
-
throw new Error("Usage: /
|
|
730
|
+
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
731
|
+
throw new Error("Usage: /skill [status|enable <id>|disable <id>|enable-all|disable-all]");
|
|
476
732
|
}
|
|
477
733
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
478
|
-
throw new Error(`Usage: /
|
|
734
|
+
throw new Error(`Usage: /skill ${verb} <id>`);
|
|
479
735
|
}
|
|
480
736
|
if (verb === "install") {
|
|
481
737
|
const row = (await skillManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd))).find((candidate) => candidate.id === normalizeRecommendedSkillId(id));
|
|
@@ -484,9 +740,44 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
484
740
|
await queueModelSkillInstall(pi, ctx, row);
|
|
485
741
|
return;
|
|
486
742
|
}
|
|
743
|
+
if (verb === "enable-all" || verb === "disable-all") {
|
|
744
|
+
const enabled = verb === "enable-all";
|
|
745
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
746
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
747
|
+
const managed = rows.filter((row) => row.source === "recommended" && row.installed && !row.blocked);
|
|
748
|
+
const locals = rows.filter((row) => row.source === "local");
|
|
749
|
+
const failed = [];
|
|
750
|
+
for (const row of managed) {
|
|
751
|
+
try {
|
|
752
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, enabled);
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
if (enabled)
|
|
756
|
+
failed.push(row.name);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (locals.length > 0) {
|
|
760
|
+
await setLocalSkillsEnabled(ctx.cwd, locals.map((row) => row.name), enabled);
|
|
761
|
+
}
|
|
762
|
+
ctx.ui.notify(enabled && failed.length > 0
|
|
763
|
+
? `已批量启用(${failed.join("、")} 启用失败)。请运行 /reload 重新加载。`
|
|
764
|
+
: `已批量${enabled ? "启用" : "停用"}可管理的 Skill。请运行 /reload 重新加载。`, enabled && failed.length > 0 ? "warning" : "info");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
487
767
|
if (verb === "enable" || verb === "disable") {
|
|
488
|
-
|
|
489
|
-
|
|
768
|
+
const requested = normalizeRecommendedSkillId(id);
|
|
769
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
770
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
771
|
+
const row = rows.find((candidate) => candidate.id === requested || candidate.name === requested || candidate.id === userSkillId(requested));
|
|
772
|
+
if (!row)
|
|
773
|
+
throw new Error(`未找到 Skill: ${id}`);
|
|
774
|
+
if (row.source === "local") {
|
|
775
|
+
await setLocalSkillEnabled(ctx.cwd, row.name, verb === "enable");
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, verb === "enable");
|
|
779
|
+
}
|
|
780
|
+
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
490
781
|
return;
|
|
491
782
|
}
|
|
492
783
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
@@ -505,6 +796,42 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
505
796
|
const action = await openSkillManager(ctx, rows);
|
|
506
797
|
if (action.type === "close")
|
|
507
798
|
return;
|
|
799
|
+
if (action.type === "toggle-all") {
|
|
800
|
+
const managed = rows.filter((row) => row.source === "recommended" && row.installed && !row.blocked);
|
|
801
|
+
const locals = rows.filter((row) => row.source === "local");
|
|
802
|
+
if (action.enabled) {
|
|
803
|
+
const failed = [];
|
|
804
|
+
for (const row of managed) {
|
|
805
|
+
try {
|
|
806
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, true);
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
failed.push(row.name);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (locals.length > 0) {
|
|
813
|
+
const ids = locals.map((row) => row.name);
|
|
814
|
+
await setLocalSkillsEnabled(ctx.cwd, ids, true);
|
|
815
|
+
}
|
|
816
|
+
ctx.ui.notify(failed.length > 0
|
|
817
|
+
? `已启用全部可管理的 Skill(${failed.join("、")} 启用失败:可能未通过来源、许可或依赖预检)。请运行 /reload 重新加载。`
|
|
818
|
+
: `已启用全部可管理的 Skill。请运行 /reload 重新加载。`, failed.length > 0 ? "warning" : "info");
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
for (const row of managed) {
|
|
822
|
+
try {
|
|
823
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, false);
|
|
824
|
+
}
|
|
825
|
+
catch { /* already disabled */ }
|
|
826
|
+
}
|
|
827
|
+
if (locals.length > 0) {
|
|
828
|
+
const ids = locals.map((row) => row.name);
|
|
829
|
+
await setLocalSkillsEnabled(ctx.cwd, ids, false);
|
|
830
|
+
}
|
|
831
|
+
ctx.ui.notify("已停用全部可管理的 Skill。请运行 /reload 重新加载。", "info");
|
|
832
|
+
}
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
508
835
|
const row = rows.find((candidate) => candidate.id === action.id);
|
|
509
836
|
if (!row)
|
|
510
837
|
continue;
|
|
@@ -513,8 +840,14 @@ async function showSkillManager(pi, args, ctx) {
|
|
|
513
840
|
return;
|
|
514
841
|
}
|
|
515
842
|
if (action.type === "toggle") {
|
|
516
|
-
|
|
517
|
-
|
|
843
|
+
if (row.source === "local") {
|
|
844
|
+
await setLocalSkillEnabled(ctx.cwd, row.name, action.enabled);
|
|
845
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"}本地 Skill ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
|
|
849
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
850
|
+
}
|
|
518
851
|
}
|
|
519
852
|
}
|
|
520
853
|
}
|
|
@@ -618,24 +951,62 @@ export default function psyclawExtension(pi) {
|
|
|
618
951
|
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
619
952
|
const runtimeMcps = new RuntimeMcpRegistry();
|
|
620
953
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
621
|
-
pi.on("resources_discover", async (event
|
|
954
|
+
pi.on("resources_discover", async (event) => {
|
|
622
955
|
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
join(event.cwd, ".agents", "skills"),
|
|
634
|
-
];
|
|
635
|
-
return { skillPaths: [...enabled.paths, ...localSkillPaths] };
|
|
956
|
+
const local = await enabledLocalSkillPaths(event.cwd, {
|
|
957
|
+
excludedNames: [...coreSkillNames(), ...await skillNamesInPaths(enabled.paths)],
|
|
958
|
+
});
|
|
959
|
+
// Return only existing Skill directories. Same-name local/core candidates
|
|
960
|
+
// are resolved before Pi sees them. Discovery warnings belong in /skill;
|
|
961
|
+
// they should not interrupt every startup.
|
|
962
|
+
return {
|
|
963
|
+
skillPaths: [...enabled.paths, ...local.paths],
|
|
964
|
+
promptPaths: await enabledLocalPromptPaths(event.cwd),
|
|
965
|
+
};
|
|
636
966
|
});
|
|
637
967
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
638
968
|
pi.on("session_shutdown", () => runtimeMcps.close());
|
|
969
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
970
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
971
|
+
const run = await readControlledRun(ctx.cwd);
|
|
972
|
+
if (!run || !toolNeedsApproval(event.toolName, event.input))
|
|
973
|
+
return;
|
|
974
|
+
const summary = toolApprovalSummary(event.toolName, event.input);
|
|
975
|
+
if (run.mode === "auto") {
|
|
976
|
+
const blockedReason = autoApprovalBlocked(event.toolName, event.input);
|
|
977
|
+
await appendApproval(ctx.cwd, {
|
|
978
|
+
kind: "tool",
|
|
979
|
+
nodeId: event.toolCallId,
|
|
980
|
+
decision: blockedReason ? "rejected" : "auto-approved",
|
|
981
|
+
actor: "auto",
|
|
982
|
+
runId: run.runId,
|
|
983
|
+
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
984
|
+
summary: blockedReason ?? summary,
|
|
985
|
+
});
|
|
986
|
+
return blockedReason ? { block: true, terminate: true, reason: blockedReason } : undefined;
|
|
987
|
+
}
|
|
988
|
+
if (!ctx.hasUI)
|
|
989
|
+
return { block: true, terminate: true, reason: "该步骤需要人在环路审批,但当前没有交互界面" };
|
|
990
|
+
const choice = await ctx.ui.select(`审批执行步骤\n${summary}`, ["批准本次执行", "拒绝并停止"], { timeout: 120_000 });
|
|
991
|
+
const approved = choice === "批准本次执行";
|
|
992
|
+
await appendApproval(ctx.cwd, {
|
|
993
|
+
kind: "tool",
|
|
994
|
+
nodeId: event.toolCallId,
|
|
995
|
+
decision: approved ? "approved" : "rejected",
|
|
996
|
+
actor: "human",
|
|
997
|
+
runId: run.runId,
|
|
998
|
+
sha256: approvalInputDigest({ toolName: event.toolName, input: event.input }),
|
|
999
|
+
summary,
|
|
1000
|
+
});
|
|
1001
|
+
return approved ? undefined : { block: true, terminate: true, reason: "用户拒绝该执行步骤或审批超时" };
|
|
1002
|
+
});
|
|
1003
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
1004
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
1005
|
+
if (!pendingInitApprovals.has(ctx.cwd) || !(await planDocumentsReady(ctx.cwd)))
|
|
1006
|
+
return;
|
|
1007
|
+
pendingInitApprovals.delete(ctx.cwd);
|
|
1008
|
+
await reviewPrimaryDocuments(ctx);
|
|
1009
|
+
});
|
|
639
1010
|
pi.registerCommand("init", {
|
|
640
1011
|
description: "初始化可追溯的研究项目",
|
|
641
1012
|
handler: async (args, ctx) => {
|
|
@@ -643,8 +1014,9 @@ export default function psyclawExtension(pi) {
|
|
|
643
1014
|
const parsed = parseInitArgs(args);
|
|
644
1015
|
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
645
1016
|
if (typeof pi.sendUserMessage === "function") {
|
|
1017
|
+
pendingInitApprovals.add(ctx.cwd);
|
|
646
1018
|
pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
647
|
-
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm}
|
|
1019
|
+
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。学术追问完成并生成主要计划文档后会进入审批;全部批准后才能 /run。`, "info");
|
|
648
1020
|
}
|
|
649
1021
|
else {
|
|
650
1022
|
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
|
|
@@ -655,20 +1027,83 @@ export default function psyclawExtension(pi) {
|
|
|
655
1027
|
}
|
|
656
1028
|
},
|
|
657
1029
|
});
|
|
1030
|
+
if (!legacyTestApi)
|
|
1031
|
+
pi.registerCommand("approve", {
|
|
1032
|
+
description: "审批 /init 生成的主要研究计划文档",
|
|
1033
|
+
handler: async (_args, ctx) => {
|
|
1034
|
+
try {
|
|
1035
|
+
await reviewPrimaryDocuments(ctx);
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
await notifyError(ctx, error);
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
658
1042
|
if (!legacyTestApi)
|
|
659
1043
|
pi.registerCommand("run", {
|
|
660
|
-
description: "
|
|
1044
|
+
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
661
1045
|
handler: async (args, ctx) => {
|
|
662
1046
|
try {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1047
|
+
const parsed = parseRunArgs(args);
|
|
1048
|
+
let project;
|
|
1049
|
+
try {
|
|
1050
|
+
project = await readProject(ctx.cwd);
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
// No `.psyclaw/project.json` (or it is corrupt): fall back to the
|
|
1054
|
+
// compliant analysis documents when present, so a project prepared
|
|
1055
|
+
// outside the /init flow can be analyzed directly.
|
|
1056
|
+
const { bootstrapProjectFromAnalysisDocs } = await import("../../project/bootstrap.js");
|
|
1057
|
+
try {
|
|
1058
|
+
project = await bootstrapProjectFromAnalysisDocs(ctx.cwd);
|
|
1059
|
+
}
|
|
1060
|
+
catch (bootstrapError) {
|
|
1061
|
+
ctx.ui.notify(bootstrapError instanceof Error ? bootstrapError.message : String(bootstrapError), "warning");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
ctx.ui.notify("检测到合规的分析文档,已自动补齐研究项目记录并启动受控流程。", "info");
|
|
1065
|
+
}
|
|
1066
|
+
const planApproval = await primaryPlanApprovalStatus(ctx.cwd);
|
|
1067
|
+
if (!planApproval.ok) {
|
|
1068
|
+
const missing = planApproval.documents.filter((item) => !item.approved).map((item) => item.path);
|
|
1069
|
+
ctx.ui.notify(`主要计划尚未批准或批准后已修改:${missing.join("、")}。请运行 /approve。`, "warning");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const available = await selectableRunSkills(ctx.cwd);
|
|
1073
|
+
let requestedSkills = parsed.requestedSkills;
|
|
1074
|
+
if (parsed.mode === "auto" && requestedSkills === undefined)
|
|
1075
|
+
requestedSkills = [];
|
|
1076
|
+
if (requestedSkills === undefined && ctx.hasUI && typeof ctx.ui.input === "function") {
|
|
1077
|
+
const answer = await ctx.ui.input("选择本次 Run 使用的 Skill(逗号分隔,可留空)", available.map((skill) => skill.name).join(", "));
|
|
1078
|
+
if (answer === undefined) {
|
|
1079
|
+
ctx.ui.notify("已取消启动受控研究流程", "info");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
requestedSkills = answer.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1083
|
+
}
|
|
1084
|
+
const selectedSkills = resolveRunSkills(requestedSkills ?? [], available);
|
|
1085
|
+
const objective = parsed.objective || project.goal;
|
|
1086
|
+
const runId = `run_${Date.now()}`;
|
|
1087
|
+
await appendApproval(ctx.cwd, {
|
|
1088
|
+
kind: "run-mode",
|
|
1089
|
+
nodeId: "run-mode",
|
|
1090
|
+
decision: parsed.mode === "auto" ? "auto-approved" : "approved",
|
|
1091
|
+
actor: parsed.mode === "auto" ? "auto" : "human",
|
|
1092
|
+
runId,
|
|
1093
|
+
sha256: approvalInputDigest({ projectId: project.id, objective, selectedSkills, mode: parsed.mode }),
|
|
1094
|
+
summary: parsed.mode === "auto" ? "用户选择自动运行模式" : "用户选择人在环路模式",
|
|
1095
|
+
});
|
|
1096
|
+
if (!(await approveRunNodes(ctx, runId, parsed.mode))) {
|
|
1097
|
+
ctx.ui.notify("运行审批被拒绝或超时,未启动。", "warning");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
await activateControlledRun(ctx.cwd, runId, project.id, objective, selectedSkills, parsed.mode);
|
|
1101
|
+
pi.appendEntry("psyclaw:controlled-run", { runId, projectId: project.id, objective, selectedSkills, mode: parsed.mode, activatedAt: new Date().toISOString() });
|
|
1102
|
+
pi.sendUserMessage(controlledRunRequest(objective, selectedSkills, parsed.mode), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1103
|
+
ctx.ui.notify(`${parsed.mode === "auto" ? "自动" : "人在环路"}研究流程已启动${selectedSkills.length > 0 ? `;本次使用 Skill:${selectedSkills.join(", ")}` : ";使用默认 Skill"}。`, "info");
|
|
669
1104
|
}
|
|
670
|
-
catch {
|
|
671
|
-
ctx
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
await notifyError(ctx, error);
|
|
672
1107
|
}
|
|
673
1108
|
},
|
|
674
1109
|
});
|
|
@@ -803,27 +1238,73 @@ export default function psyclawExtension(pi) {
|
|
|
803
1238
|
},
|
|
804
1239
|
});
|
|
805
1240
|
if (!legacyTestApi)
|
|
806
|
-
pi.registerCommand("
|
|
807
|
-
description: "
|
|
1241
|
+
pi.registerCommand("skill", {
|
|
1242
|
+
description: "管理和安装 Skill",
|
|
808
1243
|
handler: async (args, ctx) => {
|
|
809
|
-
|
|
810
|
-
|
|
1244
|
+
const [name, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1245
|
+
if (!name) {
|
|
1246
|
+
try {
|
|
1247
|
+
await showSkillManager(pi, "", ctx);
|
|
1248
|
+
}
|
|
1249
|
+
catch (error) {
|
|
1250
|
+
await notifyError(ctx, error);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
811
1253
|
}
|
|
812
|
-
|
|
813
|
-
|
|
1254
|
+
if (name === "install") {
|
|
1255
|
+
const source = rest.join(" ").trim();
|
|
1256
|
+
if (!source) {
|
|
1257
|
+
ctx.ui.notify("Usage: /skill install <local-directory>", "info");
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
const installed = await installLocalSkill(source, ctx.cwd);
|
|
1262
|
+
ctx.ui.notify(`已安装本地 Skill ${installed.name}:${installed.target}。请执行 /reload。`, "info");
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
await notifyError(ctx, error);
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
814
1268
|
}
|
|
1269
|
+
if (["status", "enable", "disable", "enable-all", "disable-all"].includes(name)) {
|
|
1270
|
+
try {
|
|
1271
|
+
await showSkillManager(pi, args, ctx);
|
|
1272
|
+
}
|
|
1273
|
+
catch (error) {
|
|
1274
|
+
await notifyError(ctx, error);
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
ctx.ui.notify("Usage: /skill [status|enable <id>|disable <id>|enable-all|disable-all|install <local-directory>]", "info");
|
|
815
1279
|
},
|
|
816
1280
|
});
|
|
817
1281
|
if (!legacyTestApi)
|
|
818
|
-
pi.registerCommand("
|
|
819
|
-
description: "
|
|
1282
|
+
pi.registerCommand("plugin", {
|
|
1283
|
+
description: "管理 Plugin / Extension",
|
|
820
1284
|
handler: async (args, ctx) => {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1285
|
+
try {
|
|
1286
|
+
const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
1287
|
+
if (!action) {
|
|
1288
|
+
ctx.ui.notify("Usage: /plugin list | install <source> [-l] | remove <source> [-l]", "info");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (!["list", "install", "remove"].includes(action)) {
|
|
1292
|
+
throw new Error("Usage: /plugin list | install <source> [-l] | remove <source> [-l]");
|
|
1293
|
+
}
|
|
1294
|
+
if (action !== "list" && rest.length === 0)
|
|
1295
|
+
throw new Error(`Usage: /plugin ${action} <source> [-l]`);
|
|
1296
|
+
if (action !== "list") {
|
|
1297
|
+
const approved = await ctx.ui.confirm(`${action === "install" ? "安装" : "移除"} Plugin?`, `${rest.join(" ")}\nPlugin 与宿主进程同权限,操作完成后需要重启 PsyClaw。`);
|
|
1298
|
+
if (!approved)
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
await runPluginCommand([action, ...rest]);
|
|
1302
|
+
if (action !== "list")
|
|
1303
|
+
ctx.ui.notify("Plugin 配置已更新,请重启 PsyClaw。", "info");
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
await notifyError(ctx, error);
|
|
825
1307
|
}
|
|
826
|
-
pi.sendUserMessage(`/skill:${name}${rest.length ? ` ${rest.join(" ")}` : ""}`, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
827
1308
|
},
|
|
828
1309
|
});
|
|
829
1310
|
if (!legacyTestApi)
|
|
@@ -831,7 +1312,7 @@ export default function psyclawExtension(pi) {
|
|
|
831
1312
|
description: "打开 MCP 安装与配置管理页",
|
|
832
1313
|
handler: async (args, ctx) => {
|
|
833
1314
|
try {
|
|
834
|
-
await showMcpManager(pi, args, ctx);
|
|
1315
|
+
await showMcpManager(pi, args, ctx, runtimeMcps);
|
|
835
1316
|
}
|
|
836
1317
|
catch (error) {
|
|
837
1318
|
await notifyError(ctx, error);
|
|
@@ -849,7 +1330,7 @@ export default function psyclawExtension(pi) {
|
|
|
849
1330
|
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
850
1331
|
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
851
1332
|
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
|
|
852
|
-
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/
|
|
1333
|
+
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/skill 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
|
|
853
1334
|
return;
|
|
854
1335
|
}
|
|
855
1336
|
if (kind !== "skill" && kind !== "mcp")
|
|
@@ -858,13 +1339,13 @@ export default function psyclawExtension(pi) {
|
|
|
858
1339
|
if (kind === "skill")
|
|
859
1340
|
await showSkillManager(pi, "", ctx);
|
|
860
1341
|
else
|
|
861
|
-
await showMcpManager(pi, "", ctx);
|
|
1342
|
+
await showMcpManager(pi, "", ctx, runtimeMcps);
|
|
862
1343
|
return;
|
|
863
1344
|
}
|
|
864
1345
|
if (kind === "skill")
|
|
865
1346
|
await showSkillManager(pi, `install ${id}`, ctx);
|
|
866
1347
|
else
|
|
867
|
-
await showMcpManager(pi, `install ${id}`, ctx);
|
|
1348
|
+
await showMcpManager(pi, `install ${id}`, ctx, runtimeMcps);
|
|
868
1349
|
}
|
|
869
1350
|
catch (error) {
|
|
870
1351
|
await notifyError(ctx, error);
|
|
@@ -933,11 +1414,19 @@ export default function psyclawExtension(pi) {
|
|
|
933
1414
|
selectedId = choice;
|
|
934
1415
|
}
|
|
935
1416
|
if (preset && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
936
|
-
const
|
|
1417
|
+
const credential = await providerCredentialSource(preset);
|
|
1418
|
+
const key = credential === "missing" ? await promptProviderKey(ctx, preset.name, preset.apiKeyEnv) : "";
|
|
937
1419
|
if (key === undefined)
|
|
938
1420
|
return;
|
|
939
1421
|
await saveProviderConfig({ ...preset, ...(key ? { apiKey: key } : {}) });
|
|
940
|
-
await
|
|
1422
|
+
const refreshed = await Promise.race([
|
|
1423
|
+
ctx.modelRegistry.refresh().then(() => true),
|
|
1424
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 5_000)),
|
|
1425
|
+
]);
|
|
1426
|
+
if (!refreshed) {
|
|
1427
|
+
ctx.ui.notify("Provider 配置已保存;模型目录刷新超时,请重新启动 PsyClaw 后使用。", "warning");
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
941
1430
|
models = ctx.modelRegistry.getAll().filter((model) => model.provider === requested);
|
|
942
1431
|
}
|
|
943
1432
|
const selected = models.find((model) => model.id === selectedId);
|
|
@@ -972,18 +1461,18 @@ export default function psyclawExtension(pi) {
|
|
|
972
1461
|
},
|
|
973
1462
|
});
|
|
974
1463
|
const traceCommand = {
|
|
975
|
-
description: "
|
|
1464
|
+
description: "导出包含正文与工具参数的完整使用路径,供 Langfuse 或 LangSmith 分析",
|
|
976
1465
|
handler: async (args, ctx) => {
|
|
977
1466
|
try {
|
|
978
1467
|
if (args.trim())
|
|
979
|
-
throw new Error("Usage: /
|
|
1468
|
+
throw new Error("Usage: /export");
|
|
980
1469
|
const result = await exportTraces({ root: ctx.cwd });
|
|
981
1470
|
ctx.ui.notify([
|
|
982
1471
|
"使用路径已导出(未上传)",
|
|
983
1472
|
`文件:${result.output}`,
|
|
984
1473
|
`轨迹:${result.traces}`,
|
|
985
1474
|
`步骤:${result.spans}`,
|
|
986
|
-
"
|
|
1475
|
+
"包含对话正文、工具参数、原始 ID 与绝对路径,便于排查各环节问题;请勿将导出文件提交到公开仓库。",
|
|
987
1476
|
].join("\n"), "info");
|
|
988
1477
|
}
|
|
989
1478
|
catch (error) {
|
|
@@ -991,7 +1480,7 @@ export default function psyclawExtension(pi) {
|
|
|
991
1480
|
}
|
|
992
1481
|
},
|
|
993
1482
|
};
|
|
994
|
-
pi.registerCommand("
|
|
1483
|
+
pi.registerCommand("export", traceCommand);
|
|
995
1484
|
if (typeof pi.registerTool === "function") {
|
|
996
1485
|
pi.registerTool({
|
|
997
1486
|
name: "psyclaw_mcp",
|
|
@@ -1260,7 +1749,10 @@ export default function psyclawExtension(pi) {
|
|
|
1260
1749
|
}
|
|
1261
1750
|
},
|
|
1262
1751
|
});
|
|
1263
|
-
|
|
1752
|
+
// Read-only multi-agent research is a user-facing capability. Keep the
|
|
1753
|
+
// destructive/developer commands gated, but do not hide the bounded agent
|
|
1754
|
+
// runner behind PSYCLAW_DEVELOPER_COMMANDS in published builds.
|
|
1755
|
+
if (!legacyTestApi)
|
|
1264
1756
|
pi.registerCommand("agents", {
|
|
1265
1757
|
description: "运行经批准的只读研究 Agent",
|
|
1266
1758
|
handler: async (args, ctx) => {
|
|
@@ -1287,7 +1779,7 @@ export default function psyclawExtension(pi) {
|
|
|
1287
1779
|
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
1288
1780
|
return;
|
|
1289
1781
|
}
|
|
1290
|
-
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate
|
|
1782
|
+
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate PsyClaw process with extensions, skills, context files, and mutating tools disabled.");
|
|
1291
1783
|
if (!approved) {
|
|
1292
1784
|
ctx.ui.notify("Agent run canceled", "info");
|
|
1293
1785
|
return;
|