psyclaw 0.27.0 → 0.27.5
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 +9 -5
- package/dist/apps/panel/index.html +78 -3406
- package/dist/src/adapters/pi/extension.js +406 -107
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/adapters/pi/rpc.js +7 -4
- package/dist/src/adapters/pi/rpc.js.map +1 -1
- package/dist/src/branding.js +7 -5
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.js +0 -5
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +0 -0
- package/dist/src/core/citations.d.ts +2 -0
- package/dist/src/core/citations.js +4 -1
- package/dist/src/core/citations.js.map +1 -1
- package/dist/src/core/docx.js +2 -2
- package/dist/src/core/docx.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/literature/archive.d.ts +30 -0
- package/dist/src/literature/archive.js +133 -0
- package/dist/src/literature/archive.js.map +1 -0
- package/dist/src/orchestration/research-agents.d.ts +32 -0
- package/dist/src/orchestration/research-agents.js +225 -0
- package/dist/src/orchestration/research-agents.js.map +1 -0
- package/dist/src/panel/extension.js +2 -52
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.d.ts +2 -0
- package/dist/src/panel/server.js +113 -74
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/hitl.js +2 -0
- package/dist/src/project/hitl.js.map +1 -1
- package/dist/src/project/paths.d.ts +2 -6
- package/dist/src/project/paths.js +4 -12
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/skills/recommended.d.ts +10 -0
- package/dist/src/skills/recommended.js +85 -7
- package/dist/src/skills/recommended.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +32 -0
- package/dist/src/telemetry/export.js +102 -1
- package/dist/src/telemetry/export.js.map +1 -1
- package/dist/src/tui/provider-picker.d.ts +48 -0
- package/dist/src/tui/provider-picker.js +115 -0
- package/dist/src/tui/provider-picker.js.map +1 -0
- package/dist/src/workflows/academic-export.d.ts +14 -0
- package/dist/src/workflows/academic-export.js +42 -0
- package/dist/src/workflows/academic-export.js.map +1 -0
- package/dist/src/workflows/meta-analysis.d.ts +1 -0
- package/dist/src/workflows/meta-analysis.js +17 -9
- package/dist/src/workflows/meta-analysis.js.map +1 -1
- package/dist/src/workflows/publish.js +14 -0
- package/dist/src/workflows/publish.js.map +1 -1
- package/package.json +1 -1
- package/skills/core/academic-grill/SKILL.md +77 -0
- package/skills/core/manifest.json +3 -2
- package/skills/recommended/catalog.json +210 -44
|
@@ -1,17 +1,18 @@
|
|
|
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,
|
|
3
|
+
import { asProject, bootstrapProject, exportTraces, 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
9
|
import { mkdir, readFile } from "node:fs/promises";
|
|
10
|
-
import { PROVIDER_PRESETS } from "../../setup.js";
|
|
10
|
+
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { coreSkillNames,
|
|
13
|
+
import { coreSkillNames, enabledRecommendedSkillPaths, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, recommendedSkillTarget, saveRecommendationState, validateModelInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
14
14
|
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
15
|
+
import { ProviderPickerComponent, SecretInputComponent, } from "../../tui/provider-picker.js";
|
|
15
16
|
const PARADIGMS = new Set([
|
|
16
17
|
"survey-observational",
|
|
17
18
|
"qualitative-thematic",
|
|
@@ -24,28 +25,28 @@ const PARADIGMS = new Set([
|
|
|
24
25
|
"policy-legal",
|
|
25
26
|
"mixed-methods",
|
|
26
27
|
]);
|
|
27
|
-
function
|
|
28
|
+
function parseInitArgs(args) {
|
|
28
29
|
const trimmed = args.trim();
|
|
29
30
|
const match = trimmed.match(/^--paradigm(?:=|\s+)(\S+)(?:\s+([\s\S]*))?$/i);
|
|
30
31
|
if (trimmed.startsWith("--")) {
|
|
31
32
|
const paradigm = match?.[1];
|
|
32
33
|
const goal = match?.[2]?.trim();
|
|
33
34
|
if (!match || !paradigm || !goal) {
|
|
34
|
-
throw new Error("Usage: /
|
|
35
|
+
throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
|
|
35
36
|
}
|
|
36
37
|
if (!PARADIGMS.has(paradigm))
|
|
37
38
|
throw new Error(`Unsupported paradigm: ${paradigm}`);
|
|
38
39
|
return { paradigm, goal };
|
|
39
40
|
}
|
|
40
41
|
if (!trimmed)
|
|
41
|
-
throw new Error("Usage: /
|
|
42
|
+
throw new Error("Usage: /init [--paradigm survey-observational] <research goal>");
|
|
42
43
|
return { paradigm: "survey-observational", goal: trimmed };
|
|
43
44
|
}
|
|
44
45
|
async function notifyError(ctx, error) {
|
|
45
46
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
46
47
|
}
|
|
47
48
|
const activeAgentRuns = new Set();
|
|
48
|
-
const CORE_SKILLS = new Set(["research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
49
|
+
const CORE_SKILLS = new Set(["academic-grill", "research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
49
50
|
function coreSkillPath(name) {
|
|
50
51
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
51
52
|
}
|
|
@@ -94,6 +95,26 @@ async function petPreference() {
|
|
|
94
95
|
return false;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
async function saveDefaultModel(provider, model) {
|
|
99
|
+
const path = join(getAgentDir(), "settings.json");
|
|
100
|
+
let settings = {};
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
103
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
104
|
+
settings = parsed;
|
|
105
|
+
}
|
|
106
|
+
catch { /* start with a new settings file */ }
|
|
107
|
+
await mkdir(dirname(path), { recursive: true });
|
|
108
|
+
await atomicWriteFile(path, `${JSON.stringify({ ...settings, defaultProvider: provider, defaultModel: model }, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
async function pickProviderItem(ctx, title, items) {
|
|
111
|
+
const result = await ctx.ui.custom((tui, theme, keybindings, done) => new ProviderPickerComponent(title, items, tui, theme, keybindings, done));
|
|
112
|
+
return result.type === "select" ? result.id : undefined;
|
|
113
|
+
}
|
|
114
|
+
async function promptProviderKey(ctx, providerName, envName) {
|
|
115
|
+
const result = await ctx.ui.custom((tui, theme, keybindings, done) => new SecretInputComponent(`配置 ${providerName}`, envName, tui, theme, keybindings, done));
|
|
116
|
+
return result.type === "submit" ? result.value.trim() : undefined;
|
|
117
|
+
}
|
|
97
118
|
function parseModelRef(args) {
|
|
98
119
|
const value = args.trim();
|
|
99
120
|
const slash = value.indexOf("/");
|
|
@@ -122,6 +143,57 @@ function researchTaskPlan(runId, objective) {
|
|
|
122
143
|
horizon: { strategy: "hierarchical-plan-act-reflect", maxIterations: 12, reflectionEvery: 1 },
|
|
123
144
|
};
|
|
124
145
|
}
|
|
146
|
+
function academicGrillRequest(subject, mode) {
|
|
147
|
+
return [
|
|
148
|
+
"先调用 psyclaw_skill 工具加载 PsyClaw 内置的 academic-grill Skill,然后严格遵循该 Skill 对我的学术研究进行追问。",
|
|
149
|
+
subject
|
|
150
|
+
? `本次要讨论的研究主题或方案:${subject}`
|
|
151
|
+
: "从当前对话、当前项目文件和已有研究状态中确定要讨论的研究主题或方案;能从这些材料查到的信息不要反问我。",
|
|
152
|
+
"从最上游、影响最大的未决问题开始。每一轮只问一个问题,同时给出你的推荐答案或推荐决策及主要取舍。",
|
|
153
|
+
"明确区分探索性与确证性研究,不把规划内容当作已有证据。直到关键分支已经解决或明确交由研究者决定后,再整理研究规格。",
|
|
154
|
+
mode === "init"
|
|
155
|
+
? "持久化模式:init。追问完成后直接更新 academic-grill Skill 规定的全部项目文档,不再询问是否更新;写入后列出实际更新的文件。"
|
|
156
|
+
: "持久化模式:review。追问完成后先展示拟更新摘要和受影响文件,并询问我是否写回;得到明确确认前不得修改项目文档。",
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
async function controlledRunPath(root) {
|
|
160
|
+
return join(projectPaths(root).root, ".psyclaw", "controlled-run.json");
|
|
161
|
+
}
|
|
162
|
+
async function readControlledRun(root) {
|
|
163
|
+
try {
|
|
164
|
+
const value = JSON.parse(await readFile(await controlledRunPath(root), "utf8"));
|
|
165
|
+
return value.schemaVersion === "psyclaw/controlled-run/v1" && value.status === "active" && typeof value.projectId === "string" && typeof value.objective === "string" && typeof value.activatedAt === "string"
|
|
166
|
+
? value
|
|
167
|
+
: null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function activateControlledRun(root, projectId, objective) {
|
|
174
|
+
const state = {
|
|
175
|
+
schemaVersion: "psyclaw/controlled-run/v1",
|
|
176
|
+
projectId,
|
|
177
|
+
objective,
|
|
178
|
+
activatedAt: new Date().toISOString(),
|
|
179
|
+
status: "active",
|
|
180
|
+
};
|
|
181
|
+
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
function controlledRunRequest(objective) {
|
|
185
|
+
return [
|
|
186
|
+
"PsyClaw 受控研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、notes/research-spec.md 和 notes/plan.md。",
|
|
187
|
+
`本次目标:${objective}`,
|
|
188
|
+
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
189
|
+
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
190
|
+
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
191
|
+
"文献调研完成并核验后,再询问是否进行全文撰写,并询问使用哪个写作 Skill;未指定时可以建议默认方案,但仍需用户确认。只有充分证据经过核验后,才能把相关主张写入论文正文。",
|
|
192
|
+
"同行评审由用户需要时另行运行 /review;不要在本轮自动审稿。/review 会再次询问使用哪个评审 Skill。最后才询问是否导出 DOCX,以及格式要求。",
|
|
193
|
+
"开放获取 PDF 自动保存到 literature/pdfs/。不得绕过付费墙;没有合法开放全文时给出 https://doi.org/<DOI> 可点击链接,提示用户通过自己的权限下载到系统给出的目标路径。论文中的每篇引用都必须完成 DOI 核验并有本地 PDF,否则列出缺失项并保持正文或发布状态为 blocked。",
|
|
194
|
+
"如果用户在未完成文献调研、全文写作或评审前要求导出 DOCX,允许导出当前分析报告,但必须明确标为遵循学术规范的分析报告,不得称为论文。",
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
125
197
|
async function recommendedItems(kind) {
|
|
126
198
|
const file = kind === "skills" ? "catalog.json" : "mcp-catalog.json";
|
|
127
199
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -136,43 +208,52 @@ async function recommendedItems(kind) {
|
|
|
136
208
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
137
209
|
return {
|
|
138
210
|
items: Array.isArray(value.items) ? value.items.filter((item) => Boolean(item && typeof item === "object")) : [],
|
|
211
|
+
externalTools: Array.isArray(value.externalTools) ? value.externalTools.filter((item) => Boolean(item && typeof item === "object")) : [],
|
|
139
212
|
installPrep: Array.isArray(value.installPrep) ? value.installPrep.filter((item) => Boolean(item && typeof item === "object")) : [],
|
|
140
213
|
};
|
|
141
214
|
}
|
|
142
215
|
catch { /* try next candidate */ }
|
|
143
216
|
}
|
|
144
|
-
return { items: [], installPrep: [] };
|
|
217
|
+
return { items: [], externalTools: [], installPrep: [] };
|
|
218
|
+
}
|
|
219
|
+
function skillScopeLabel(scope) {
|
|
220
|
+
return scope === "user" ? "系统目录(所有项目)" : "项目目录(仅当前项目)";
|
|
145
221
|
}
|
|
146
222
|
async function skillManagerRows(root, state) {
|
|
147
223
|
const catalog = await readRecommendedCatalog();
|
|
148
224
|
return Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
|
|
149
225
|
const id = normalizeRecommendedSkillId(item.id);
|
|
226
|
+
const scope = state.skillScopes?.[id] ?? "project";
|
|
150
227
|
try {
|
|
151
|
-
await
|
|
228
|
+
await validateModelInstalledRecommendedSkill(root, id, scope);
|
|
152
229
|
return {
|
|
153
230
|
id,
|
|
154
231
|
name: item.name,
|
|
155
232
|
description: String(item.description ?? ""),
|
|
156
233
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
234
|
+
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
235
|
+
collection: item.skillLayout === "collection",
|
|
157
236
|
installed: true,
|
|
158
237
|
enabled: state.skills.includes(id),
|
|
238
|
+
scope,
|
|
159
239
|
blocked: false,
|
|
160
240
|
};
|
|
161
241
|
}
|
|
162
|
-
catch
|
|
163
|
-
const plan = catalog.installPrep.find((candidate) => normalizeRecommendedSkillId(candidate.id) === id);
|
|
164
|
-
const installable = plan?.sourceKind === "github" && typeof plan.sourceUrl === "string" &&
|
|
165
|
-
typeof plan.skillPath === "string" && typeof plan.skillName === "string" &&
|
|
166
|
-
typeof plan.ref === "string" && !["unknown", "NOASSERTION"].includes(plan.license);
|
|
242
|
+
catch {
|
|
167
243
|
return {
|
|
168
244
|
id,
|
|
169
245
|
name: item.name,
|
|
170
246
|
description: String(item.description ?? ""),
|
|
171
247
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
248
|
+
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
249
|
+
collection: item.skillLayout === "collection",
|
|
172
250
|
installed: false,
|
|
173
251
|
enabled: false,
|
|
174
|
-
|
|
175
|
-
|
|
252
|
+
scope,
|
|
253
|
+
blocked: typeof item.sourceRef !== "string" || !/^https:\/\//.test(item.sourceRef),
|
|
254
|
+
reason: typeof item.sourceRef === "string" && /^https:\/\//.test(item.sourceRef)
|
|
255
|
+
? "尚未安装;按 Enter 选择安装范围并交给当前模型处理"
|
|
256
|
+
: "没有可交给模型检查的来源网址",
|
|
176
257
|
};
|
|
177
258
|
}
|
|
178
259
|
}));
|
|
@@ -182,7 +263,6 @@ async function mcpManagerRows(state) {
|
|
|
182
263
|
return items.map((item) => {
|
|
183
264
|
const id = String(item.id ?? "");
|
|
184
265
|
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
185
|
-
const blocked = plan?.status === "blocked";
|
|
186
266
|
const dependencies = Array.isArray(plan?.dependencies)
|
|
187
267
|
? plan.dependencies.filter((value) => typeof value === "string")
|
|
188
268
|
: [];
|
|
@@ -192,12 +272,10 @@ async function mcpManagerRows(state) {
|
|
|
192
272
|
description: String(item.description ?? ""),
|
|
193
273
|
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
194
274
|
enabled: state.mcp.includes(id),
|
|
195
|
-
blocked,
|
|
196
|
-
...(typeof plan?.blockedReason === "string" ? { reason: plan.blockedReason } : {}),
|
|
197
275
|
details: [
|
|
198
276
|
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
199
|
-
|
|
200
|
-
|
|
277
|
+
`参考版本:${String(plan?.ref ?? "由模型读取来源确定")} · 许可证:${String(plan?.license ?? "由模型读取来源确定")}`,
|
|
278
|
+
`可能需要:${dependencies.length > 0 ? dependencies.join(", ") : "由模型读取来源确定"}`,
|
|
201
279
|
],
|
|
202
280
|
};
|
|
203
281
|
});
|
|
@@ -206,17 +284,19 @@ function skillRowLabel(row) {
|
|
|
206
284
|
const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
|
|
207
285
|
return `${state} ${row.name} [${row.id}]`;
|
|
208
286
|
}
|
|
209
|
-
async function setRecommendedSkillEnabled(root, requestedId, enabled) {
|
|
287
|
+
async function setRecommendedSkillEnabled(root, requestedId, enabled, requestedScope) {
|
|
210
288
|
const id = normalizeRecommendedSkillId(requestedId);
|
|
211
|
-
if (enabled)
|
|
212
|
-
await validateInstalledRecommendedSkill(root, id);
|
|
213
289
|
const state = await readRecommendationState(root);
|
|
290
|
+
const scope = requestedScope ?? state.skillScopes?.[id] ?? "project";
|
|
291
|
+
if (enabled)
|
|
292
|
+
await validateModelInstalledRecommendedSkill(root, id, scope);
|
|
214
293
|
const current = new Set(state.skills);
|
|
215
294
|
if (enabled)
|
|
216
295
|
current.add(id);
|
|
217
296
|
else
|
|
218
297
|
current.delete(id);
|
|
219
298
|
state.skills = [...current];
|
|
299
|
+
state.skillScopes = { ...(state.skillScopes ?? {}), [id]: scope };
|
|
220
300
|
await saveRecommendationState(root, state);
|
|
221
301
|
}
|
|
222
302
|
function skillManagerItems(rows) {
|
|
@@ -234,6 +314,7 @@ function skillManagerItems(rows) {
|
|
|
234
314
|
description: row.description,
|
|
235
315
|
status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
|
|
236
316
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
317
|
+
details: [`安装位置:${skillScopeLabel(row.scope)}`],
|
|
237
318
|
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
238
319
|
})),
|
|
239
320
|
];
|
|
@@ -246,10 +327,9 @@ function mcpManagerItems(rows) {
|
|
|
246
327
|
id: row.id,
|
|
247
328
|
name: row.name,
|
|
248
329
|
description: row.description,
|
|
249
|
-
status: row.
|
|
330
|
+
status: row.enabled ? "enabled" : "disabled",
|
|
250
331
|
configuredEnabled: row.enabled,
|
|
251
332
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
252
|
-
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
253
333
|
details: row.details,
|
|
254
334
|
}));
|
|
255
335
|
}
|
|
@@ -257,11 +337,10 @@ async function openMcpManager(ctx, rows) {
|
|
|
257
337
|
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
258
338
|
title: "MCP 管理",
|
|
259
339
|
itemLabel: "MCP",
|
|
260
|
-
footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter
|
|
340
|
+
footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter 交给模型安装并配置 · Esc 关闭",
|
|
261
341
|
enterAction: "install",
|
|
262
|
-
enabledText: "
|
|
342
|
+
enabledText: "项目配置已开启;Enter 可让当前模型重新安装或修复配置",
|
|
263
343
|
disabledText: "项目配置已关闭",
|
|
264
|
-
blockedText: "预检阻断;若配置曾开启,可按 Space 关闭",
|
|
265
344
|
})));
|
|
266
345
|
}
|
|
267
346
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
@@ -270,8 +349,6 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
270
349
|
const row = rows.find((candidate) => candidate.id === id);
|
|
271
350
|
if (!row)
|
|
272
351
|
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
273
|
-
if (enabled && row.blocked)
|
|
274
|
-
throw new Error(row.reason ?? `MCP 预检未通过: ${id}`);
|
|
275
352
|
const current = new Set(state.mcp);
|
|
276
353
|
if (enabled)
|
|
277
354
|
current.add(id);
|
|
@@ -280,23 +357,29 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
280
357
|
state.mcp = [...current];
|
|
281
358
|
await saveRecommendationState(root, state);
|
|
282
359
|
}
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
};
|
|
360
|
+
function modelMcpInstallTask(root, row, plan) {
|
|
361
|
+
const suggestedCommand = typeof plan?.command === "string" ? plan.command : "请根据来源仓库的最新安装说明确定";
|
|
362
|
+
const suggestedTarget = typeof plan?.target === "string" ? plan.target : `.psyclaw/mcp/${row.id}.json`;
|
|
363
|
+
return [
|
|
364
|
+
`安装并配置推荐 MCP:${row.name} (${row.id})。`,
|
|
365
|
+
`来源网址:${row.sourceRef ?? "未提供;请停止并告知用户"}`,
|
|
366
|
+
`当前项目:${root}`,
|
|
367
|
+
`目录中的参考命令:${suggestedCommand}`,
|
|
368
|
+
`建议的项目配置记录:${suggestedTarget}`,
|
|
369
|
+
"用户已经通过 MCP 管理页明确授权本次安装。你可以直接联网、克隆或下载来源、使用所需包管理器安装依赖、执行安装命令,并写入与该 MCP 有关的项目或用户级运行配置;不要再次要求用户批准这些安装步骤。",
|
|
370
|
+
"请先阅读来源仓库的实际安装与启动说明,再根据当前操作系统和宿主环境完成可运行的 stdio/HTTP MCP 配置。目录中的版本、命令和依赖只作参考,不得因为哈希、许可证、依赖或本地数据策略检查而中止安装。",
|
|
371
|
+
"安装完成后实际启动一次服务器或执行等价的最小连通性检查,确认启动命令和工作目录有效。不要输出、复制或提交 API Key、令牌及其他凭据。",
|
|
372
|
+
`成功后确保 .psyclaw/recommendations.json 的 mcp 列表包含 ${JSON.stringify(row.id)},说明写入了哪些配置,并提醒用户执行 /reload。若安装命令本身失败,直接诊断并修复;只有遇到必须由用户提供的凭据或外部软件许可证时才向用户说明。`,
|
|
373
|
+
].join("\n");
|
|
298
374
|
}
|
|
299
|
-
async function
|
|
375
|
+
async function queueModelMcpInstall(pi, ctx, row, plan) {
|
|
376
|
+
if (!row.sourceRef)
|
|
377
|
+
throw new Error(`推荐 MCP 没有来源网址: ${row.id}`);
|
|
378
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, true);
|
|
379
|
+
pi.sendUserMessage(modelMcpInstallTask(ctx.cwd, row, plan), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
380
|
+
ctx.ui.notify(`已将 ${row.name} 的下载、安装和配置任务交给当前模型。模型完成并确认可启动后,请执行 /reload。`, "info");
|
|
381
|
+
}
|
|
382
|
+
async function showMcpManager(pi, args, ctx) {
|
|
300
383
|
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
301
384
|
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
302
385
|
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
|
|
@@ -306,7 +389,11 @@ async function showMcpManager(args, ctx) {
|
|
|
306
389
|
}
|
|
307
390
|
const catalog = await recommendedItems("mcp");
|
|
308
391
|
if (verb === "install") {
|
|
309
|
-
|
|
392
|
+
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
393
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
394
|
+
if (!row)
|
|
395
|
+
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
396
|
+
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
310
397
|
return;
|
|
311
398
|
}
|
|
312
399
|
if (verb === "enable" || verb === "disable") {
|
|
@@ -316,7 +403,7 @@ async function showMcpManager(args, ctx) {
|
|
|
316
403
|
}
|
|
317
404
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
318
405
|
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
319
|
-
ctx.ui.notify(rows.map((row) => `${row.
|
|
406
|
+
ctx.ui.notify(rows.map((row) => `${row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}`).join("\n"), "info");
|
|
320
407
|
return;
|
|
321
408
|
}
|
|
322
409
|
while (true) {
|
|
@@ -328,8 +415,8 @@ async function showMcpManager(args, ctx) {
|
|
|
328
415
|
if (!row)
|
|
329
416
|
continue;
|
|
330
417
|
if (action.type === "install") {
|
|
331
|
-
|
|
332
|
-
|
|
418
|
+
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
419
|
+
return;
|
|
333
420
|
}
|
|
334
421
|
if (action.type === "toggle") {
|
|
335
422
|
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
@@ -337,7 +424,48 @@ async function showMcpManager(args, ctx) {
|
|
|
337
424
|
}
|
|
338
425
|
}
|
|
339
426
|
}
|
|
340
|
-
|
|
427
|
+
function modelSkillInstallTask(root, row, scope) {
|
|
428
|
+
const target = recommendedSkillTarget(root, row.id, scope);
|
|
429
|
+
return [
|
|
430
|
+
`安装推荐 Skill:${row.name} (${row.id})。`,
|
|
431
|
+
`来源网址:${row.sourceRef}`,
|
|
432
|
+
...(row.installHint ? [`仓库入口提示:${row.installHint}`] : []),
|
|
433
|
+
`安装位置:${skillScopeLabel(scope)}。`,
|
|
434
|
+
`唯一允许的最终目标目录:${target}`,
|
|
435
|
+
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
|
|
436
|
+
row.collection
|
|
437
|
+
? "这是多 Skill 套件:目标目录自身无需 SKILL.md,但其子目录必须包含一个或多个有效 SKILL.md。保留套件内共享目录和相对路径,不得包含 .git、符号链接或凭据。"
|
|
438
|
+
: "目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
|
|
439
|
+
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
440
|
+
"安装完成后检查目标目录结构,并提醒用户执行 /skills 启用该项,再执行 /reload。",
|
|
441
|
+
].join("\n");
|
|
442
|
+
}
|
|
443
|
+
async function chooseSkillScope(ctx) {
|
|
444
|
+
const projectLabel = skillScopeLabel("project");
|
|
445
|
+
const systemLabel = skillScopeLabel("user");
|
|
446
|
+
const selected = await ctx.ui.select("选择 Skill 安装位置", [projectLabel, systemLabel]);
|
|
447
|
+
if (selected === projectLabel)
|
|
448
|
+
return "project";
|
|
449
|
+
if (selected === systemLabel)
|
|
450
|
+
return "user";
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
async function queueModelSkillInstall(pi, ctx, row) {
|
|
454
|
+
if (!row.sourceRef)
|
|
455
|
+
throw new Error(`推荐 Skill 没有来源网址: ${row.id}`);
|
|
456
|
+
const scope = await chooseSkillScope(ctx);
|
|
457
|
+
if (!scope)
|
|
458
|
+
return;
|
|
459
|
+
const approved = await ctx.ui.confirm("交给当前模型安装 Skill?", `${row.name}\n来源:${row.sourceRef}\n安装位置:${skillScopeLabel(scope)}\n目标目录:${recommendedSkillTarget(ctx.cwd, row.id, scope)}\n模型将检查仓库并使用文件与命令工具完成安装。`);
|
|
460
|
+
if (!approved)
|
|
461
|
+
return;
|
|
462
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
463
|
+
state.skillScopes = { ...(state.skillScopes ?? {}), [row.id]: scope };
|
|
464
|
+
await saveRecommendationState(ctx.cwd, state);
|
|
465
|
+
pi.sendUserMessage(modelSkillInstallTask(ctx.cwd, row, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
466
|
+
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /skills 中启用,再执行 /reload。`, "info");
|
|
467
|
+
}
|
|
468
|
+
async function showSkillManager(pi, args, ctx) {
|
|
341
469
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
342
470
|
const verb = action[0];
|
|
343
471
|
const id = action[1];
|
|
@@ -348,13 +476,15 @@ async function showSkillManager(args, ctx) {
|
|
|
348
476
|
throw new Error(`Usage: /skills ${verb} <id>`);
|
|
349
477
|
}
|
|
350
478
|
if (verb === "install") {
|
|
351
|
-
const
|
|
352
|
-
|
|
479
|
+
const row = (await skillManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd))).find((candidate) => candidate.id === normalizeRecommendedSkillId(id));
|
|
480
|
+
if (!row)
|
|
481
|
+
throw new Error(`未找到推荐 Skill: ${id}`);
|
|
482
|
+
await queueModelSkillInstall(pi, ctx, row);
|
|
353
483
|
return;
|
|
354
484
|
}
|
|
355
485
|
if (verb === "enable" || verb === "disable") {
|
|
356
486
|
await setRecommendedSkillEnabled(ctx.cwd, id, verb === "enable");
|
|
357
|
-
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${normalizeRecommendedSkillId(id)}
|
|
487
|
+
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${normalizeRecommendedSkillId(id)}。请运行 /reload 重新加载。`, "info");
|
|
358
488
|
return;
|
|
359
489
|
}
|
|
360
490
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
@@ -377,21 +507,12 @@ async function showSkillManager(args, ctx) {
|
|
|
377
507
|
if (!row)
|
|
378
508
|
continue;
|
|
379
509
|
if (action.type === "install") {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
continue;
|
|
383
|
-
try {
|
|
384
|
-
const result = await installRecommendedSkill(ctx.cwd, row.id);
|
|
385
|
-
ctx.ui.notify(`${result.installed ? "安装完成" : "已经安装"}:${result.skillName}`, "info");
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
await notifyError(ctx, error);
|
|
389
|
-
}
|
|
390
|
-
continue;
|
|
510
|
+
await queueModelSkillInstall(pi, ctx, row);
|
|
511
|
+
return;
|
|
391
512
|
}
|
|
392
513
|
if (action.type === "toggle") {
|
|
393
514
|
await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
|
|
394
|
-
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name}
|
|
515
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name}。请运行 /reload 重新加载。`, "info");
|
|
395
516
|
}
|
|
396
517
|
}
|
|
397
518
|
}
|
|
@@ -493,19 +614,49 @@ const WORKFLOW_RUNNERS = {
|
|
|
493
614
|
export default function psyclawExtension(pi) {
|
|
494
615
|
const developerCommands = process.env.PSYCLAW_DEVELOPER_COMMANDS === "1";
|
|
495
616
|
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
496
|
-
pi.
|
|
497
|
-
|
|
617
|
+
if (!legacyTestApi && typeof pi.on === "function")
|
|
618
|
+
pi.on("resources_discover", async (event, ctx) => {
|
|
619
|
+
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
620
|
+
for (const warning of enabled.warnings)
|
|
621
|
+
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
622
|
+
return { skillPaths: enabled.paths };
|
|
623
|
+
});
|
|
624
|
+
pi.registerCommand("init", {
|
|
625
|
+
description: "初始化可追溯的研究项目",
|
|
498
626
|
handler: async (args, ctx) => {
|
|
499
627
|
try {
|
|
500
|
-
const parsed =
|
|
628
|
+
const parsed = parseInitArgs(args);
|
|
501
629
|
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
502
|
-
|
|
630
|
+
if (typeof pi.sendUserMessage === "function") {
|
|
631
|
+
pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
632
|
+
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。现在开始学术追问;准备好后使用 /run 启动受控研究流程。`, "info");
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
|
|
636
|
+
}
|
|
503
637
|
}
|
|
504
638
|
catch (error) {
|
|
505
639
|
await notifyError(ctx, error);
|
|
506
640
|
}
|
|
507
641
|
},
|
|
508
642
|
});
|
|
643
|
+
if (!legacyTestApi)
|
|
644
|
+
pi.registerCommand("run", {
|
|
645
|
+
description: "在 /init 后启动受控研究流程",
|
|
646
|
+
handler: async (args, ctx) => {
|
|
647
|
+
try {
|
|
648
|
+
const project = await readProject(ctx.cwd);
|
|
649
|
+
const objective = args.trim() || project.goal;
|
|
650
|
+
await activateControlledRun(ctx.cwd, project.id, objective);
|
|
651
|
+
pi.appendEntry("psyclaw:controlled-run", { projectId: project.id, objective, activatedAt: new Date().toISOString() });
|
|
652
|
+
pi.sendUserMessage(controlledRunRequest(objective), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
653
|
+
ctx.ui.notify("受控研究流程已启动。分析完成后会先询问是否撰写论文;不会直接进入 DOCX 导出。", "info");
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
ctx.ui.notify("请先使用 /init 初始化研究项目;未运行 /init 和 /run 时保持普通对话模式。", "warning");
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
});
|
|
509
660
|
if (developerCommands || legacyTestApi)
|
|
510
661
|
pi.registerCommand("verify", {
|
|
511
662
|
description: "写入机器可读的交接检查点",
|
|
@@ -543,12 +694,105 @@ export default function psyclawExtension(pi) {
|
|
|
543
694
|
}
|
|
544
695
|
},
|
|
545
696
|
});
|
|
697
|
+
if (!legacyTestApi)
|
|
698
|
+
pi.registerCommand("grill", {
|
|
699
|
+
description: "逐题追问并压力测试学术研究方案",
|
|
700
|
+
handler: async (args, ctx) => {
|
|
701
|
+
const subject = args.trim();
|
|
702
|
+
pi.sendUserMessage(academicGrillRequest(subject, "review"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
703
|
+
ctx.ui.notify("已启动学术追问。完成后会先展示文档更新建议,并询问是否写回。", "info");
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
if (!legacyTestApi)
|
|
707
|
+
pi.registerCommand("review", {
|
|
708
|
+
description: "并发运行多角色模拟同行评审",
|
|
709
|
+
handler: async (_args, ctx) => {
|
|
710
|
+
if (activeAgentRuns.has(ctx.cwd)) {
|
|
711
|
+
ctx.ui.notify("当前项目已有一个多智能体任务正在运行", "warning");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const project = await readProject(ctx.cwd);
|
|
716
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
717
|
+
ctx.ui.notify("请先使用 /run 启动受控研究流程;普通对话模式不会自动执行同行评审。", "warning");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const reviewSkill = ctx.hasUI
|
|
721
|
+
? await ctx.ui.input("同行评审使用哪个 Skill?", "留空使用 PsyClaw 默认多角色模拟评审")
|
|
722
|
+
: undefined;
|
|
723
|
+
if (reviewSkill === undefined && ctx.hasUI) {
|
|
724
|
+
ctx.ui.notify("已取消同行评审", "info");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
activeAgentRuns.add(ctx.cwd);
|
|
728
|
+
ctx.ui.notify(`正在使用${reviewSkill?.trim() ? ` Skill“${reviewSkill.trim()}”辅助的` : "默认"} 4 角色并发模拟评审;不会自动改稿。`, "info");
|
|
729
|
+
const result = await runParallelPeerReview({
|
|
730
|
+
root: ctx.cwd,
|
|
731
|
+
project,
|
|
732
|
+
...(ctx.model?.provider === undefined ? {} : { provider: ctx.model.provider }),
|
|
733
|
+
...(ctx.model?.id === undefined ? {} : { model: ctx.model.id }),
|
|
734
|
+
env: providerEnvironment(ctx.model?.provider),
|
|
735
|
+
...(reviewSkill?.trim() ? { skillGuidance: reviewSkill.trim() } : {}),
|
|
736
|
+
onEvent: async (event) => { await new RunEventLog(ctx.cwd, event.runId).append(event); },
|
|
737
|
+
});
|
|
738
|
+
pi.appendEntry("psyclaw:multi-agent-review", {
|
|
739
|
+
runId: result.runId,
|
|
740
|
+
status: result.status,
|
|
741
|
+
outputPath: result.outputPath,
|
|
742
|
+
adoption: "pending-human-decision",
|
|
743
|
+
recordedAt: new Date().toISOString(),
|
|
744
|
+
});
|
|
745
|
+
ctx.ui.notify(`模拟审稿${result.status === "completed" ? "已完成" : "未完整完成"}:${result.outputPath}。审稿意见仅供研究者决定是否采纳。`, result.status === "completed" ? "info" : "warning");
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
await notifyError(ctx, error);
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
activeAgentRuns.delete(ctx.cwd);
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
if (!legacyTestApi)
|
|
756
|
+
pi.registerCommand("loop", {
|
|
757
|
+
description: "按计划循环推进当前研究阶段",
|
|
758
|
+
handler: async (args, ctx) => {
|
|
759
|
+
const requested = args.trim();
|
|
760
|
+
if (requested.toLowerCase() === "stop") {
|
|
761
|
+
if (ctx.isIdle())
|
|
762
|
+
ctx.ui.notify("当前没有正在运行的研究循环", "info");
|
|
763
|
+
else {
|
|
764
|
+
ctx.abort();
|
|
765
|
+
ctx.ui.notify("已请求停止当前研究循环", "warning");
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
let project;
|
|
770
|
+
try {
|
|
771
|
+
project = await readProject(ctx.cwd);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /loop", "warning");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const objective = requested || project.goal;
|
|
778
|
+
const request = [
|
|
779
|
+
"运行一个有边界的 PsyClaw 研究循环,采用计划、执行、检查、修正的顺序推进当前研究阶段。",
|
|
780
|
+
`本轮目标:${objective}`,
|
|
781
|
+
"开始前读取 .psyclaw/project.json、notes/goal.md 和 notes/plan.md;如果当前对话或项目中存在经研究者确认的 /grill 研究规格,以该规格细化初始计划,但不得违反系统规则、AGENTS.md 或研究门禁。",
|
|
782
|
+
"先识别当前最高优先级且依赖已满足的一个任务,再使用可用工具实际推进。不要只描述准备做什么。普通项目文件写入限于该任务所需范围;原始数据、凭据、外部发布、破坏性操作和方法学关键变更仍需明确批准。",
|
|
783
|
+
"每完成一个实质步骤就检查产物、证据和计划是否一致。遇到缺失证据、研究者决策、权限边界、不可恢复错误或当前阶段已经完成时立即停止,不得为了继续循环而虚构输入或结论。",
|
|
784
|
+
"本次最多推进一个可验收的研究阶段。结束时简要报告已完成、已核验、阻塞、计划变化和下一步;规划内容不得表述为已有研究证据。",
|
|
785
|
+
].join("\n");
|
|
786
|
+
pi.sendUserMessage(request, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
787
|
+
ctx.ui.notify("已启动有界研究循环;使用 /loop stop 可请求中止", "info");
|
|
788
|
+
},
|
|
789
|
+
});
|
|
546
790
|
if (!legacyTestApi)
|
|
547
791
|
pi.registerCommand("skills", {
|
|
548
792
|
description: "打开 Skill 安装与启用管理页",
|
|
549
793
|
handler: async (args, ctx) => {
|
|
550
794
|
try {
|
|
551
|
-
await showSkillManager(args, ctx);
|
|
795
|
+
await showSkillManager(pi, args, ctx);
|
|
552
796
|
}
|
|
553
797
|
catch (error) {
|
|
554
798
|
await notifyError(ctx, error);
|
|
@@ -557,10 +801,10 @@ export default function psyclawExtension(pi) {
|
|
|
557
801
|
});
|
|
558
802
|
if (!legacyTestApi)
|
|
559
803
|
pi.registerCommand("mcp", {
|
|
560
|
-
description: "打开 MCP
|
|
804
|
+
description: "打开 MCP 安装与配置管理页",
|
|
561
805
|
handler: async (args, ctx) => {
|
|
562
806
|
try {
|
|
563
|
-
await showMcpManager(args, ctx);
|
|
807
|
+
await showMcpManager(pi, args, ctx);
|
|
564
808
|
}
|
|
565
809
|
catch (error) {
|
|
566
810
|
await notifyError(ctx, error);
|
|
@@ -569,7 +813,7 @@ export default function psyclawExtension(pi) {
|
|
|
569
813
|
});
|
|
570
814
|
if (!legacyTestApi)
|
|
571
815
|
pi.registerCommand("install", {
|
|
572
|
-
description: "查看推荐 Skill/MCP
|
|
816
|
+
description: "查看推荐 Skill/MCP,或交给当前模型安装",
|
|
573
817
|
handler: async (args, ctx) => {
|
|
574
818
|
try {
|
|
575
819
|
const [kind, id] = args.trim().split(/\s+/, 2);
|
|
@@ -577,22 +821,23 @@ export default function psyclawExtension(pi) {
|
|
|
577
821
|
const [skills, mcps] = await Promise.all([recommendedItems("skills"), recommendedItems("mcp")]);
|
|
578
822
|
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
579
823
|
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
580
|
-
|
|
824
|
+
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
|
|
825
|
+
ctx.ui.notify(["推荐安装入口", "", ...skillLines, ...mcpLines, "", ...toolLines, "", "安装:/install skill|mcp <id>", "管理:/skills 或 /mcp", "外部工具不通过 Skill 安装器安装", "也可以打开 /panel 查看推荐页面"].join("\n"), "info");
|
|
581
826
|
return;
|
|
582
827
|
}
|
|
583
828
|
if (kind !== "skill" && kind !== "mcp")
|
|
584
829
|
throw new Error("Usage: /install skill|mcp <id>");
|
|
585
830
|
if (!id) {
|
|
586
831
|
if (kind === "skill")
|
|
587
|
-
await showSkillManager("", ctx);
|
|
832
|
+
await showSkillManager(pi, "", ctx);
|
|
588
833
|
else
|
|
589
|
-
await showMcpManager("", ctx);
|
|
834
|
+
await showMcpManager(pi, "", ctx);
|
|
590
835
|
return;
|
|
591
836
|
}
|
|
592
837
|
if (kind === "skill")
|
|
593
|
-
await showSkillManager(`install ${id}`, ctx);
|
|
838
|
+
await showSkillManager(pi, `install ${id}`, ctx);
|
|
594
839
|
else
|
|
595
|
-
await showMcpManager(`install ${id}`, ctx);
|
|
840
|
+
await showMcpManager(pi, `install ${id}`, ctx);
|
|
596
841
|
}
|
|
597
842
|
catch (error) {
|
|
598
843
|
await notifyError(ctx, error);
|
|
@@ -604,7 +849,7 @@ export default function psyclawExtension(pi) {
|
|
|
604
849
|
description: "查看或切换模型 Provider",
|
|
605
850
|
handler: async (args, ctx) => {
|
|
606
851
|
try {
|
|
607
|
-
|
|
852
|
+
let requested = args.trim();
|
|
608
853
|
const providers = new Map();
|
|
609
854
|
for (const model of ctx.modelRegistry.getAll()) {
|
|
610
855
|
const list = providers.get(model.provider) ?? [];
|
|
@@ -613,33 +858,69 @@ export default function psyclawExtension(pi) {
|
|
|
613
858
|
}
|
|
614
859
|
if (!requested) {
|
|
615
860
|
const current = ctx.model?.provider ?? "none";
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
861
|
+
const available = new Map(PROVIDER_PRESETS.filter((preset) => preset.models.length > 0).map((preset) => [preset.id, preset]));
|
|
862
|
+
for (const id of providers.keys())
|
|
863
|
+
if (!available.has(id))
|
|
864
|
+
available.set(id, {
|
|
865
|
+
id,
|
|
866
|
+
name: ctx.modelRegistry.getProviderDisplayName(id),
|
|
867
|
+
baseUrl: "",
|
|
868
|
+
api: "openai-completions",
|
|
869
|
+
apiKeyEnv: `${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`,
|
|
870
|
+
models: [],
|
|
871
|
+
});
|
|
872
|
+
if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
|
|
873
|
+
const lines = [...available].map(([id, preset]) => `${id}${id === current ? " *" : ""} — ${preset.name}`);
|
|
874
|
+
ctx.ui.notify([`当前 Provider: ${current}`, ...lines, "", "切换:/provider <id>"].join("\n"), "info");
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const selectedProvider = await pickProviderItem(ctx, "选择模型 Provider", [...available].map(([id, preset]) => ({
|
|
878
|
+
id,
|
|
879
|
+
label: preset.name,
|
|
880
|
+
description: `${id} · ${providers.get(id)?.length ?? preset.models.length} 个模型`,
|
|
881
|
+
current: id === current,
|
|
882
|
+
})));
|
|
883
|
+
if (!selectedProvider)
|
|
884
|
+
return;
|
|
885
|
+
requested = selectedProvider;
|
|
619
886
|
}
|
|
620
887
|
if (!/^[A-Za-z0-9._:-]+$/.test(requested))
|
|
621
888
|
throw new Error("Usage: /provider <provider-id>");
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
let selectedId =
|
|
628
|
-
if (
|
|
629
|
-
if (!ctx.hasUI)
|
|
889
|
+
const preset = PROVIDER_PRESETS.find((item) => item.id === requested);
|
|
890
|
+
let models = providers.get(requested) ?? [];
|
|
891
|
+
const modelChoices = models.length > 0 ? models : (preset?.models ?? []);
|
|
892
|
+
if (modelChoices.length === 0)
|
|
893
|
+
throw new Error(`Unknown provider or no configured models: ${requested}`);
|
|
894
|
+
let selectedId = modelChoices[0].id;
|
|
895
|
+
if (modelChoices.length > 1) {
|
|
896
|
+
if (!ctx.hasUI || typeof ctx.ui.custom !== "function")
|
|
630
897
|
throw new Error(`Provider ${requested} has multiple models; use /model ${requested}/<model-id>`);
|
|
631
|
-
const choice = await ctx
|
|
898
|
+
const choice = await pickProviderItem(ctx, `选择 ${preset?.name ?? requested} 模型`, modelChoices.map((model) => ({
|
|
899
|
+
id: model.id,
|
|
900
|
+
label: "name" in model && typeof model.name === "string" ? model.name : model.id,
|
|
901
|
+
description: model.id,
|
|
902
|
+
current: ctx.model?.provider === requested && ctx.model.id === model.id,
|
|
903
|
+
})));
|
|
632
904
|
if (!choice)
|
|
633
905
|
return;
|
|
634
906
|
selectedId = choice;
|
|
635
907
|
}
|
|
908
|
+
if (preset && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
909
|
+
const key = await promptProviderKey(ctx, preset.name, preset.apiKeyEnv);
|
|
910
|
+
if (key === undefined)
|
|
911
|
+
return;
|
|
912
|
+
await saveProviderConfig({ ...preset, ...(key ? { apiKey: key } : {}) });
|
|
913
|
+
await ctx.modelRegistry.refresh();
|
|
914
|
+
models = ctx.modelRegistry.getAll().filter((model) => model.provider === requested);
|
|
915
|
+
}
|
|
636
916
|
const selected = models.find((model) => model.id === selectedId);
|
|
637
917
|
if (!selected)
|
|
638
918
|
throw new Error(`No model selected for provider: ${requested}`);
|
|
639
919
|
const changed = await pi.setModel(selected);
|
|
640
920
|
if (!changed)
|
|
641
|
-
throw new Error(
|
|
642
|
-
|
|
921
|
+
throw new Error(`未找到 ${requested} 的可用凭据;请重新运行 /provider 并输入 API Key`);
|
|
922
|
+
await saveDefaultModel(requested, selected.id);
|
|
923
|
+
ctx.ui.notify(`已切换并设为默认模型:${requested}/${selected.id}`, "info");
|
|
643
924
|
}
|
|
644
925
|
catch (error) {
|
|
645
926
|
await notifyError(ctx, error);
|
|
@@ -690,7 +971,7 @@ export default function psyclawExtension(pi) {
|
|
|
690
971
|
label: "Research skill",
|
|
691
972
|
description: "Load one trusted bundled psyclaw core skill and make its use visible to the user. Use this instead of directly reading a core SKILL.md file.",
|
|
692
973
|
parameters: Type.Object({
|
|
693
|
-
name: Type.String({ description: "Core skill name: research-intake, evidence-capture, citation-audit, or research-brief" }),
|
|
974
|
+
name: Type.String({ description: "Core skill name: academic-grill, research-intake, evidence-capture, citation-audit, or research-brief" }),
|
|
694
975
|
purpose: Type.Optional(Type.String({ description: "Short user-facing reason for using this skill now" })),
|
|
695
976
|
}),
|
|
696
977
|
executionMode: "sequential",
|
|
@@ -732,6 +1013,16 @@ export default function psyclawExtension(pi) {
|
|
|
732
1013
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
733
1014
|
try {
|
|
734
1015
|
const request = params.request.trim();
|
|
1016
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1017
|
+
return {
|
|
1018
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1019
|
+
schemaVersion: "psyclaw/workbench-routing/v1",
|
|
1020
|
+
status: "general-mode",
|
|
1021
|
+
message: "PsyClaw controlled research mode is inactive. Continue as a normal conversational agent; do not impose research workflow gates. The user can run /init and then /run when they want the controlled workflow.",
|
|
1022
|
+
}, null, 2) }],
|
|
1023
|
+
details: { status: "general-mode", request },
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
735
1026
|
const combined = `${request} ${params.identifier ?? ""}`;
|
|
736
1027
|
const explicitWorkflow = params.workflow?.trim();
|
|
737
1028
|
if (explicitWorkflow) {
|
|
@@ -755,20 +1046,19 @@ export default function psyclawExtension(pi) {
|
|
|
755
1046
|
}
|
|
756
1047
|
if (publishIntent) {
|
|
757
1048
|
try {
|
|
758
|
-
const result = await
|
|
1049
|
+
const result = await exportAcademicDocument(ctx.cwd);
|
|
759
1050
|
return {
|
|
760
1051
|
content: [{ type: "text", text: JSON.stringify({
|
|
761
1052
|
schemaVersion: "psyclaw/publish-result/v1",
|
|
762
|
-
workflow: "publish-manuscript",
|
|
1053
|
+
workflow: result.kind === "manuscript" ? "publish-manuscript" : "export-analysis-report",
|
|
763
1054
|
status: "completed",
|
|
764
1055
|
markdownPath: result.markdownPath,
|
|
765
1056
|
docxPath: result.docxPath,
|
|
766
|
-
markdownSha256: result.markdownSha256,
|
|
767
1057
|
docxSha256: result.docxSha256,
|
|
768
|
-
|
|
769
|
-
|
|
1058
|
+
classification: result.kind,
|
|
1059
|
+
...(result.kind === "analysis-report" ? { notice: result.notice } : {}),
|
|
770
1060
|
}, null, 2) }],
|
|
771
|
-
details: { workflow: "publish-manuscript", markdownPath: result.markdownPath, docxPath: result.docxPath
|
|
1061
|
+
details: { workflow: result.kind === "manuscript" ? "publish-manuscript" : "export-analysis-report", markdownPath: result.markdownPath, docxPath: result.docxPath },
|
|
772
1062
|
};
|
|
773
1063
|
}
|
|
774
1064
|
catch (error) {
|
|
@@ -847,6 +1137,12 @@ export default function psyclawExtension(pi) {
|
|
|
847
1137
|
executionMode: "sequential",
|
|
848
1138
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
849
1139
|
try {
|
|
1140
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1141
|
+
return {
|
|
1142
|
+
content: [{ type: "text", text: "当前是普通对话模式;请先运行 /init,再运行 /run,之后才会启用引用归档与论文证据门禁。" }],
|
|
1143
|
+
details: { status: "general-mode" },
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
850
1146
|
const result = await recordCitationUse(ctx.cwd, {
|
|
851
1147
|
doi: params.doi,
|
|
852
1148
|
reason: params.reason,
|
|
@@ -863,9 +1159,12 @@ export default function psyclawExtension(pi) {
|
|
|
863
1159
|
reason: result.record.reason,
|
|
864
1160
|
referenceTitle: ref?.title ?? null,
|
|
865
1161
|
archived: ref !== null,
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1162
|
+
fulltext: result.fulltext,
|
|
1163
|
+
next: result.record.verified && result.fulltext?.status === "downloaded"
|
|
1164
|
+
? `引用已双源核验,开放全文已保存到 ${result.fulltext.localPath}。`
|
|
1165
|
+
: result.fulltext?.status === "manual-download-required"
|
|
1166
|
+
? `该引用的题录已登记,但未找到开放 PDF。请通过 ${result.fulltext.doiUrl} 使用你的合法访问权限下载,并保存到 ${result.fulltext.localPath};完成前不得将依赖全文的主张写入正文。`
|
|
1167
|
+
: "该 DOI 或全文未能完成核验;请在最终交付前补核验或替换来源。",
|
|
869
1168
|
}, null, 2) }],
|
|
870
1169
|
details: { citationId: result.record.citationId, doi: result.record.doi, verified: result.record.verified },
|
|
871
1170
|
};
|
|
@@ -920,13 +1219,13 @@ export default function psyclawExtension(pi) {
|
|
|
920
1219
|
return;
|
|
921
1220
|
}
|
|
922
1221
|
// Do not let the runner create a partial `.psyclaw/runs` tree for an
|
|
923
|
-
// uninitialized or corrupt project.
|
|
1222
|
+
// uninitialized or corrupt project. `/init` is
|
|
924
1223
|
// the explicit state-creation step.
|
|
925
1224
|
try {
|
|
926
1225
|
await readProject(ctx.cwd);
|
|
927
1226
|
}
|
|
928
1227
|
catch {
|
|
929
|
-
ctx.ui.notify("
|
|
1228
|
+
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
930
1229
|
return;
|
|
931
1230
|
}
|
|
932
1231
|
const approved = await ctx.ui.confirm("Run read-only research worker?", "The worker runs in a separate Pi process with extensions, skills, context files, and mutating tools disabled.");
|