psyclaw 0.27.4 → 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 -3449
- package/dist/src/adapters/pi/extension.js +328 -76
- 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 +6 -4
- package/dist/src/branding.js.map +1 -1
- 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/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 +0 -52
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.js +47 -65
- 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 +1 -0
- package/dist/src/skills/recommended.js +40 -10
- 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 +56 -9
|
@@ -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
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));
|
|
@@ -159,6 +231,8 @@ async function skillManagerRows(root, state) {
|
|
|
159
231
|
name: item.name,
|
|
160
232
|
description: String(item.description ?? ""),
|
|
161
233
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
234
|
+
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
235
|
+
collection: item.skillLayout === "collection",
|
|
162
236
|
installed: true,
|
|
163
237
|
enabled: state.skills.includes(id),
|
|
164
238
|
scope,
|
|
@@ -171,6 +245,8 @@ async function skillManagerRows(root, state) {
|
|
|
171
245
|
name: item.name,
|
|
172
246
|
description: String(item.description ?? ""),
|
|
173
247
|
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
248
|
+
...(typeof item.installHint === "string" ? { installHint: item.installHint } : {}),
|
|
249
|
+
collection: item.skillLayout === "collection",
|
|
174
250
|
installed: false,
|
|
175
251
|
enabled: false,
|
|
176
252
|
scope,
|
|
@@ -187,7 +263,6 @@ async function mcpManagerRows(state) {
|
|
|
187
263
|
return items.map((item) => {
|
|
188
264
|
const id = String(item.id ?? "");
|
|
189
265
|
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
190
|
-
const blocked = plan?.status === "blocked";
|
|
191
266
|
const dependencies = Array.isArray(plan?.dependencies)
|
|
192
267
|
? plan.dependencies.filter((value) => typeof value === "string")
|
|
193
268
|
: [];
|
|
@@ -197,12 +272,10 @@ async function mcpManagerRows(state) {
|
|
|
197
272
|
description: String(item.description ?? ""),
|
|
198
273
|
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
199
274
|
enabled: state.mcp.includes(id),
|
|
200
|
-
blocked,
|
|
201
|
-
...(typeof plan?.blockedReason === "string" ? { reason: plan.blockedReason } : {}),
|
|
202
275
|
details: [
|
|
203
276
|
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
204
|
-
|
|
205
|
-
|
|
277
|
+
`参考版本:${String(plan?.ref ?? "由模型读取来源确定")} · 许可证:${String(plan?.license ?? "由模型读取来源确定")}`,
|
|
278
|
+
`可能需要:${dependencies.length > 0 ? dependencies.join(", ") : "由模型读取来源确定"}`,
|
|
206
279
|
],
|
|
207
280
|
};
|
|
208
281
|
});
|
|
@@ -254,10 +327,9 @@ function mcpManagerItems(rows) {
|
|
|
254
327
|
id: row.id,
|
|
255
328
|
name: row.name,
|
|
256
329
|
description: row.description,
|
|
257
|
-
status: row.
|
|
330
|
+
status: row.enabled ? "enabled" : "disabled",
|
|
258
331
|
configuredEnabled: row.enabled,
|
|
259
332
|
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
260
|
-
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
261
333
|
details: row.details,
|
|
262
334
|
}));
|
|
263
335
|
}
|
|
@@ -265,11 +337,10 @@ async function openMcpManager(ctx, rows) {
|
|
|
265
337
|
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
266
338
|
title: "MCP 管理",
|
|
267
339
|
itemLabel: "MCP",
|
|
268
|
-
footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter
|
|
340
|
+
footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter 交给模型安装并配置 · Esc 关闭",
|
|
269
341
|
enterAction: "install",
|
|
270
|
-
enabledText: "
|
|
342
|
+
enabledText: "项目配置已开启;Enter 可让当前模型重新安装或修复配置",
|
|
271
343
|
disabledText: "项目配置已关闭",
|
|
272
|
-
blockedText: "预检阻断;若配置曾开启,可按 Space 关闭",
|
|
273
344
|
})));
|
|
274
345
|
}
|
|
275
346
|
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
@@ -278,8 +349,6 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
278
349
|
const row = rows.find((candidate) => candidate.id === id);
|
|
279
350
|
if (!row)
|
|
280
351
|
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
281
|
-
if (enabled && row.blocked)
|
|
282
|
-
throw new Error(row.reason ?? `MCP 预检未通过: ${id}`);
|
|
283
352
|
const current = new Set(state.mcp);
|
|
284
353
|
if (enabled)
|
|
285
354
|
current.add(id);
|
|
@@ -288,23 +357,29 @@ async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
|
288
357
|
state.mcp = [...current];
|
|
289
358
|
await saveRecommendationState(root, state);
|
|
290
359
|
}
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
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");
|
|
306
374
|
}
|
|
307
|
-
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) {
|
|
308
383
|
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
309
384
|
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
310
385
|
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
|
|
@@ -314,7 +389,11 @@ async function showMcpManager(args, ctx) {
|
|
|
314
389
|
}
|
|
315
390
|
const catalog = await recommendedItems("mcp");
|
|
316
391
|
if (verb === "install") {
|
|
317
|
-
|
|
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));
|
|
318
397
|
return;
|
|
319
398
|
}
|
|
320
399
|
if (verb === "enable" || verb === "disable") {
|
|
@@ -324,7 +403,7 @@ async function showMcpManager(args, ctx) {
|
|
|
324
403
|
}
|
|
325
404
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
326
405
|
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
327
|
-
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");
|
|
328
407
|
return;
|
|
329
408
|
}
|
|
330
409
|
while (true) {
|
|
@@ -336,8 +415,8 @@ async function showMcpManager(args, ctx) {
|
|
|
336
415
|
if (!row)
|
|
337
416
|
continue;
|
|
338
417
|
if (action.type === "install") {
|
|
339
|
-
|
|
340
|
-
|
|
418
|
+
await queueModelMcpInstall(pi, ctx, row, catalog.installPrep.find((candidate) => candidate.id === row.id));
|
|
419
|
+
return;
|
|
341
420
|
}
|
|
342
421
|
if (action.type === "toggle") {
|
|
343
422
|
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
@@ -350,10 +429,13 @@ function modelSkillInstallTask(root, row, scope) {
|
|
|
350
429
|
return [
|
|
351
430
|
`安装推荐 Skill:${row.name} (${row.id})。`,
|
|
352
431
|
`来源网址:${row.sourceRef}`,
|
|
432
|
+
...(row.installHint ? [`仓库入口提示:${row.installHint}`] : []),
|
|
353
433
|
`安装位置:${skillScopeLabel(scope)}。`,
|
|
354
434
|
`唯一允许的最终目标目录:${target}`,
|
|
355
|
-
"
|
|
356
|
-
|
|
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、符号链接、凭据或二进制大文件。",
|
|
357
439
|
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
358
440
|
"安装完成后检查目标目录结构,并提醒用户执行 /skills 启用该项,再执行 /reload。",
|
|
359
441
|
].join("\n");
|
|
@@ -539,19 +621,42 @@ export default function psyclawExtension(pi) {
|
|
|
539
621
|
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
540
622
|
return { skillPaths: enabled.paths };
|
|
541
623
|
});
|
|
542
|
-
pi.registerCommand("
|
|
543
|
-
description: "
|
|
624
|
+
pi.registerCommand("init", {
|
|
625
|
+
description: "初始化可追溯的研究项目",
|
|
544
626
|
handler: async (args, ctx) => {
|
|
545
627
|
try {
|
|
546
|
-
const parsed =
|
|
628
|
+
const parsed = parseInitArgs(args);
|
|
547
629
|
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
548
|
-
|
|
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
|
+
}
|
|
549
637
|
}
|
|
550
638
|
catch (error) {
|
|
551
639
|
await notifyError(ctx, error);
|
|
552
640
|
}
|
|
553
641
|
},
|
|
554
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
|
+
});
|
|
555
660
|
if (developerCommands || legacyTestApi)
|
|
556
661
|
pi.registerCommand("verify", {
|
|
557
662
|
description: "写入机器可读的交接检查点",
|
|
@@ -589,6 +694,99 @@ export default function psyclawExtension(pi) {
|
|
|
589
694
|
}
|
|
590
695
|
},
|
|
591
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
|
+
});
|
|
592
790
|
if (!legacyTestApi)
|
|
593
791
|
pi.registerCommand("skills", {
|
|
594
792
|
description: "打开 Skill 安装与启用管理页",
|
|
@@ -603,10 +801,10 @@ export default function psyclawExtension(pi) {
|
|
|
603
801
|
});
|
|
604
802
|
if (!legacyTestApi)
|
|
605
803
|
pi.registerCommand("mcp", {
|
|
606
|
-
description: "打开 MCP
|
|
804
|
+
description: "打开 MCP 安装与配置管理页",
|
|
607
805
|
handler: async (args, ctx) => {
|
|
608
806
|
try {
|
|
609
|
-
await showMcpManager(args, ctx);
|
|
807
|
+
await showMcpManager(pi, args, ctx);
|
|
610
808
|
}
|
|
611
809
|
catch (error) {
|
|
612
810
|
await notifyError(ctx, error);
|
|
@@ -615,7 +813,7 @@ export default function psyclawExtension(pi) {
|
|
|
615
813
|
});
|
|
616
814
|
if (!legacyTestApi)
|
|
617
815
|
pi.registerCommand("install", {
|
|
618
|
-
description: "查看推荐 Skill/MCP
|
|
816
|
+
description: "查看推荐 Skill/MCP,或交给当前模型安装",
|
|
619
817
|
handler: async (args, ctx) => {
|
|
620
818
|
try {
|
|
621
819
|
const [kind, id] = args.trim().split(/\s+/, 2);
|
|
@@ -633,13 +831,13 @@ export default function psyclawExtension(pi) {
|
|
|
633
831
|
if (kind === "skill")
|
|
634
832
|
await showSkillManager(pi, "", ctx);
|
|
635
833
|
else
|
|
636
|
-
await showMcpManager("", ctx);
|
|
834
|
+
await showMcpManager(pi, "", ctx);
|
|
637
835
|
return;
|
|
638
836
|
}
|
|
639
837
|
if (kind === "skill")
|
|
640
838
|
await showSkillManager(pi, `install ${id}`, ctx);
|
|
641
839
|
else
|
|
642
|
-
await showMcpManager(`install ${id}`, ctx);
|
|
840
|
+
await showMcpManager(pi, `install ${id}`, ctx);
|
|
643
841
|
}
|
|
644
842
|
catch (error) {
|
|
645
843
|
await notifyError(ctx, error);
|
|
@@ -651,7 +849,7 @@ export default function psyclawExtension(pi) {
|
|
|
651
849
|
description: "查看或切换模型 Provider",
|
|
652
850
|
handler: async (args, ctx) => {
|
|
653
851
|
try {
|
|
654
|
-
|
|
852
|
+
let requested = args.trim();
|
|
655
853
|
const providers = new Map();
|
|
656
854
|
for (const model of ctx.modelRegistry.getAll()) {
|
|
657
855
|
const list = providers.get(model.provider) ?? [];
|
|
@@ -660,33 +858,69 @@ export default function psyclawExtension(pi) {
|
|
|
660
858
|
}
|
|
661
859
|
if (!requested) {
|
|
662
860
|
const current = ctx.model?.provider ?? "none";
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
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;
|
|
666
886
|
}
|
|
667
887
|
if (!/^[A-Za-z0-9._:-]+$/.test(requested))
|
|
668
888
|
throw new Error("Usage: /provider <provider-id>");
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
let selectedId =
|
|
675
|
-
if (
|
|
676
|
-
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")
|
|
677
897
|
throw new Error(`Provider ${requested} has multiple models; use /model ${requested}/<model-id>`);
|
|
678
|
-
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
|
+
})));
|
|
679
904
|
if (!choice)
|
|
680
905
|
return;
|
|
681
906
|
selectedId = choice;
|
|
682
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
|
+
}
|
|
683
916
|
const selected = models.find((model) => model.id === selectedId);
|
|
684
917
|
if (!selected)
|
|
685
918
|
throw new Error(`No model selected for provider: ${requested}`);
|
|
686
919
|
const changed = await pi.setModel(selected);
|
|
687
920
|
if (!changed)
|
|
688
|
-
throw new Error(
|
|
689
|
-
|
|
921
|
+
throw new Error(`未找到 ${requested} 的可用凭据;请重新运行 /provider 并输入 API Key`);
|
|
922
|
+
await saveDefaultModel(requested, selected.id);
|
|
923
|
+
ctx.ui.notify(`已切换并设为默认模型:${requested}/${selected.id}`, "info");
|
|
690
924
|
}
|
|
691
925
|
catch (error) {
|
|
692
926
|
await notifyError(ctx, error);
|
|
@@ -737,7 +971,7 @@ export default function psyclawExtension(pi) {
|
|
|
737
971
|
label: "Research skill",
|
|
738
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.",
|
|
739
973
|
parameters: Type.Object({
|
|
740
|
-
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" }),
|
|
741
975
|
purpose: Type.Optional(Type.String({ description: "Short user-facing reason for using this skill now" })),
|
|
742
976
|
}),
|
|
743
977
|
executionMode: "sequential",
|
|
@@ -779,6 +1013,16 @@ export default function psyclawExtension(pi) {
|
|
|
779
1013
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
780
1014
|
try {
|
|
781
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
|
+
}
|
|
782
1026
|
const combined = `${request} ${params.identifier ?? ""}`;
|
|
783
1027
|
const explicitWorkflow = params.workflow?.trim();
|
|
784
1028
|
if (explicitWorkflow) {
|
|
@@ -802,20 +1046,19 @@ export default function psyclawExtension(pi) {
|
|
|
802
1046
|
}
|
|
803
1047
|
if (publishIntent) {
|
|
804
1048
|
try {
|
|
805
|
-
const result = await
|
|
1049
|
+
const result = await exportAcademicDocument(ctx.cwd);
|
|
806
1050
|
return {
|
|
807
1051
|
content: [{ type: "text", text: JSON.stringify({
|
|
808
1052
|
schemaVersion: "psyclaw/publish-result/v1",
|
|
809
|
-
workflow: "publish-manuscript",
|
|
1053
|
+
workflow: result.kind === "manuscript" ? "publish-manuscript" : "export-analysis-report",
|
|
810
1054
|
status: "completed",
|
|
811
1055
|
markdownPath: result.markdownPath,
|
|
812
1056
|
docxPath: result.docxPath,
|
|
813
|
-
markdownSha256: result.markdownSha256,
|
|
814
1057
|
docxSha256: result.docxSha256,
|
|
815
|
-
|
|
816
|
-
|
|
1058
|
+
classification: result.kind,
|
|
1059
|
+
...(result.kind === "analysis-report" ? { notice: result.notice } : {}),
|
|
817
1060
|
}, null, 2) }],
|
|
818
|
-
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 },
|
|
819
1062
|
};
|
|
820
1063
|
}
|
|
821
1064
|
catch (error) {
|
|
@@ -894,6 +1137,12 @@ export default function psyclawExtension(pi) {
|
|
|
894
1137
|
executionMode: "sequential",
|
|
895
1138
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
896
1139
|
try {
|
|
1140
|
+
if (!(await readControlledRun(ctx.cwd))) {
|
|
1141
|
+
return {
|
|
1142
|
+
content: [{ type: "text", text: "当前是普通对话模式;请先运行 /init,再运行 /run,之后才会启用引用归档与论文证据门禁。" }],
|
|
1143
|
+
details: { status: "general-mode" },
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
897
1146
|
const result = await recordCitationUse(ctx.cwd, {
|
|
898
1147
|
doi: params.doi,
|
|
899
1148
|
reason: params.reason,
|
|
@@ -910,9 +1159,12 @@ export default function psyclawExtension(pi) {
|
|
|
910
1159
|
reason: result.record.reason,
|
|
911
1160
|
referenceTitle: ref?.title ?? null,
|
|
912
1161
|
archived: ref !== null,
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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 或全文未能完成核验;请在最终交付前补核验或替换来源。",
|
|
916
1168
|
}, null, 2) }],
|
|
917
1169
|
details: { citationId: result.record.citationId, doi: result.record.doi, verified: result.record.verified },
|
|
918
1170
|
};
|
|
@@ -967,13 +1219,13 @@ export default function psyclawExtension(pi) {
|
|
|
967
1219
|
return;
|
|
968
1220
|
}
|
|
969
1221
|
// Do not let the runner create a partial `.psyclaw/runs` tree for an
|
|
970
|
-
// uninitialized or corrupt project.
|
|
1222
|
+
// uninitialized or corrupt project. `/init` is
|
|
971
1223
|
// the explicit state-creation step.
|
|
972
1224
|
try {
|
|
973
1225
|
await readProject(ctx.cwd);
|
|
974
1226
|
}
|
|
975
1227
|
catch {
|
|
976
|
-
ctx.ui.notify("
|
|
1228
|
+
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
977
1229
|
return;
|
|
978
1230
|
}
|
|
979
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.");
|