psyclaw 0.27.7 → 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 +14 -11
- package/dist/src/adapters/pi/extension.js +274 -51
- 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 +20 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -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/orchestration/runner.js +1 -0
- package/dist/src/orchestration/runner.js.map +1 -1
- package/dist/src/panel/server.js +16 -10
- 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/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 +9 -1
- package/dist/src/skills/user-skills.js +48 -2
- package/dist/src/skills/user-skills.js.map +1 -1
- package/dist/src/style/cli-ui.js +19 -30
- package/dist/src/style/cli-ui.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/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 +29 -1
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# PsyClaw
|
|
2
2
|
|
|
3
|
-
PsyClaw
|
|
3
|
+
PsyClaw 是面向社会科学研究的智能体工作台。它把研究项目、证据来源、Claim-Evidence 账本、完整性门禁、人工裁决、可恢复工作流和本地面板接到内置运行时上;它不替代统计软件,也不会把未经核验的引用、结果或审稿意见写成事实。
|
|
4
4
|
|
|
5
|
-
当前版本:`0.27.
|
|
5
|
+
当前版本:`0.27.12`。正式命令、用户配置目录和后续发布统一使用 `psyclaw`。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
需要 Node.js `>=22.19.0`。官方 npm 源:
|
|
10
10
|
|
|
11
11
|
```powershell
|
|
12
|
-
npm install -g psyclaw@0.27.
|
|
12
|
+
npm install -g psyclaw@0.27.12
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
中国大陆网络较慢或无法访问官方源时:
|
|
16
16
|
|
|
17
17
|
```powershell
|
|
18
|
-
npm install -g psyclaw@0.27.
|
|
18
|
+
npm install -g psyclaw@0.27.12 --registry=https://registry.npmmirror.com
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
确认命令入口:
|
|
@@ -24,7 +24,7 @@ npm install -g psyclaw@0.27.7 --registry=https://registry.npmmirror.com
|
|
|
24
24
|
psyclaw --help
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
直接运行 `psyclaw` 会启动交互研究工作台;首次没有模型配置时会进入配置向导。向导会检查当前进程、macOS `launchctl` 和已有用户凭据,也允许直接遮罩输入 Key。`launchctl` Key 只注入当前运行进程、不落盘;登录 shell 仅在用户明确按 `Ctrl+I` 后读取。直接输入或明确导入的 Key 只保存到
|
|
27
|
+
直接运行 `psyclaw` 会启动交互研究工作台;首次没有模型配置时会进入配置向导。向导会检查当前进程、macOS `launchctl` 和已有用户凭据,也允许直接遮罩输入 Key。`launchctl` Key 只注入当前运行进程、不落盘;登录 shell 仅在用户明确按 `Ctrl+I` 后读取。直接输入或明确导入的 Key 只保存到 PsyClaw 用户级 `auth.json`,不会写入项目或 `models.json`。也可显式运行 `psyclaw wizard`,或使用 `psyclaw setup --provider deepseek` 写入仅引用环境变量的预设。不要把 API key 写进命令参数、项目、README 或 Git 仓库。
|
|
28
28
|
|
|
29
29
|
## 五分钟开始
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ psyclaw init "社交支持与研究生心理健康的关系" --paradigm survey-o
|
|
|
37
37
|
psyclaw
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
初始化会建立 `.psyclaw
|
|
40
|
+
初始化会建立 `.psyclaw/data/raw/`、`data/clean/`、`literature/pdfs/`、`notes/` 和 `outputs/`,不再把原始数据目录暴露在项目根目录。`.psyclaw/data/raw/` 是只读保护区;旧项目的 `data/raw/` 仍保持只读兼容,把经确认可用于分析的派生数据放在 `data/clean/`。
|
|
41
41
|
|
|
42
42
|
在对话界面中,`/init <研究目标>` 只初始化研究项目和规格,随后使用 `/run [本轮目标]` 才启动受控研究流程。没有依次运行 `/init` 和 `/run` 时,PsyClaw 保持普通对话模式,不强制加入研究门禁或阶段式询问。
|
|
43
43
|
|
|
@@ -61,16 +61,19 @@ psyclaw brief
|
|
|
61
61
|
| 创建人工裁决模板 | `psyclaw hitl init` |
|
|
62
62
|
| 写研究移交记录 | `psyclaw handoff` |
|
|
63
63
|
| 扫描本机其他 Agent | `psyclaw agents` |
|
|
64
|
-
| 查看 PsyClaw
|
|
65
|
-
| 更新 PsyClaw
|
|
64
|
+
| 查看 PsyClaw 和内置运行时更新 | `psyclaw check-updates` |
|
|
65
|
+
| 更新 PsyClaw 和内置运行时 | `psyclaw update` |
|
|
66
66
|
| 导出完整使用路径 | 在对话中输入 `/export` |
|
|
67
67
|
| 打开科研面板 | 在对话中输入 `/panel` |
|
|
68
68
|
| 查看或切换 Provider | 在对话中输入 `/provider` 或 `/provider <id>` |
|
|
69
69
|
| 管理启动横幅宠物 | `/pet status`、`/pet on`、`/pet off`(默认关闭) |
|
|
70
|
-
|
|
|
70
|
+
| 安装本地 Skill | `/skill install <本地目录>` |
|
|
71
|
+
| 管理或安装 Skill | `/skill` 或 `/skill install <本地目录>` |
|
|
72
|
+
| 调用已加载 Skill | `/skill:<name>` |
|
|
73
|
+
| 管理 Plugin | `/plugin`;终端使用 `psyclaw plugin install|remove|list` |
|
|
71
74
|
| 调用已配置 MCP | 模型通过 `psyclaw_mcp` 自动发现并调用 `.psyclaw/mcp/*.json` 中启用的服务器 |
|
|
72
75
|
|
|
73
|
-
`psyclaw update` 会更新 PsyClaw
|
|
76
|
+
`psyclaw update` 会更新 PsyClaw 整包,并同时安装该版本锁定的内置运行时。在源码仓库中运行时,命令会停止并提示通过 Git 更新,不会覆盖本地修改。
|
|
74
77
|
仅查看更新计划而不执行时,使用 `psyclaw update --check`。旧的 `--yes` 参数仍兼容,但不再需要。
|
|
75
78
|
|
|
76
79
|
### 导出使用路径
|
|
@@ -79,7 +82,7 @@ psyclaw brief
|
|
|
79
82
|
/export
|
|
80
83
|
```
|
|
81
84
|
|
|
82
|
-
该斜杠命令直接在 PsyClaw
|
|
85
|
+
该斜杠命令直接在 PsyClaw 对话界面中使用,从当前项目的工作流日志和对应会话中生成 `outputs/psyclaw-export.otlp.json`,用于通过 OpenTelemetry Collector 导入 Langfuse 或 LangSmith。导出文件包含对话正文、工具参数、研究内容、原始 ID 与绝对路径,只写入本地,不会自动上传;不要将其提交到公开仓库。终端入口为 `psyclaw export`。Panel 不再读取实时 Trace,只展示 `outputs/` 下已经显式生成的 Export 文件。
|
|
83
86
|
|
|
84
87
|
## 开发者本地安装
|
|
85
88
|
|
|
@@ -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
9
|
import { lstat, mkdir, readFile } from "node:fs/promises";
|
|
10
|
-
import { PROVIDER_PRESETS, saveProviderConfig } from "../../setup.js";
|
|
10
|
+
import { PROVIDER_PRESETS, providerCredentialSource, saveProviderConfig } from "../../setup.js";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } 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 { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, userSkillId, } from "../../skills/user-skills.js";
|
|
15
|
+
import { enabledLocalSkillPaths, enabledLocalPromptPaths, readUserSkillState, scanLocalSkills, setLocalSkillEnabled, setLocalSkillsEnabled, skillNamesInPaths, installLocalSkill, userSkillId, } from "../../skills/user-skills.js";
|
|
16
16
|
import { RuntimeMcpRegistry, setUserMcpConfigEnabled, } from "../../integrations/mcp-runtime.js";
|
|
17
|
-
import {
|
|
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,32 +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, selectedSkills: Array.isArray(value.selectedSkills) ? value.selectedSkills.filter((item) => typeof item === "string") : [] }
|
|
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, selectedSkills) {
|
|
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,
|
|
180
193
|
selectedSkills: [...new Set(selectedSkills)],
|
|
194
|
+
mode,
|
|
181
195
|
activatedAt: new Date().toISOString(),
|
|
182
196
|
status: "active",
|
|
183
197
|
};
|
|
184
198
|
await atomicWriteFile(await controlledRunPath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
185
199
|
return state;
|
|
186
200
|
}
|
|
187
|
-
function controlledRunRequest(objective, selectedSkills) {
|
|
201
|
+
function controlledRunRequest(objective, selectedSkills, mode) {
|
|
188
202
|
return [
|
|
189
|
-
|
|
203
|
+
`PsyClaw ${mode === "auto" ? "自动" : "人在环路"}研究流程已由用户通过 /run 明确启动。先读取 .psyclaw/project.json、.psyclaw/controlled-run.json、.psyclaw/approvals.jsonl、notes/research-spec.md 和 notes/plan.md。`,
|
|
190
204
|
`本次目标:${objective}`,
|
|
191
205
|
selectedSkills.length > 0
|
|
192
206
|
? `本次运行由用户选择的优化 Skill:${selectedSkills.join(", ")}。在相关任务中优先加载并遵循这些 Skill;同名 Skill 只加载一次。`
|
|
193
207
|
: "本次运行未指定额外 Skill,使用 PsyClaw 默认研究流程。不要删除、停用或改写已安装的其他 Skill。",
|
|
208
|
+
mode === "auto"
|
|
209
|
+
? "自动模式:计划内普通步骤连续推进,不再逐项询问;运行时会自动记录审批。原始数据、凭据、破坏性命令、伦理决定、未经核验的事实与外部发布仍必须停止并请求用户决定。"
|
|
210
|
+
: "人在环路模式:每个有副作用或关键方法决策的步骤都必须通过交互审批;拒绝或超时立即停止,不得绕过审批。",
|
|
194
211
|
"实际推进当前尚未完成的研究任务;先形成或更新符合学术规范的 Markdown 分析报告,不能只描述计划。分析报告必须清楚区分数据来源、统计结果、文献证据、限制与尚未核验内容。",
|
|
195
212
|
"分析报告完成后,下一步不是直接询问是否导出 DOCX,而是询问用户是否据此撰写论文。",
|
|
196
213
|
"若用户选择撰写论文,再单独询问是否先进行文献调研,并说明该阶段可能花费较长时间;提示用户可以指定已启用的 Skill,或者选择 PsyClaw 默认的文献调研方式。不要在获得答复前自动开始长时调研。",
|
|
@@ -202,16 +219,92 @@ function controlledRunRequest(objective, selectedSkills) {
|
|
|
202
219
|
}
|
|
203
220
|
function parseRunArgs(args) {
|
|
204
221
|
const value = args.trim();
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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]*))?$/);
|
|
208
227
|
if (!match)
|
|
209
|
-
throw new Error("Usage: /run [--skills skill-a,skill-b] [objective]");
|
|
228
|
+
throw new Error("Usage: /run [auto] [--skills skill-a,skill-b] [objective]");
|
|
210
229
|
return {
|
|
211
230
|
objective: (match[2] ?? "").trim(),
|
|
212
231
|
requestedSkills: [...new Set(match[1].split(",").map((item) => item.trim()).filter(Boolean))],
|
|
232
|
+
mode: auto ? "auto" : "human",
|
|
213
233
|
};
|
|
214
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
|
+
}
|
|
215
308
|
async function selectableRunSkills(root) {
|
|
216
309
|
const state = await readRecommendationState(root);
|
|
217
310
|
const rows = await skillManagerRows(root, state);
|
|
@@ -244,7 +337,7 @@ function resolveRunSkills(requested, available) {
|
|
|
244
337
|
selected.push(name);
|
|
245
338
|
}
|
|
246
339
|
if (unknown.length > 0)
|
|
247
|
-
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /
|
|
340
|
+
throw new Error(`本次运行不可用的 Skill:${unknown.join(", ")}。请先通过 /skill 安装或启用。`);
|
|
248
341
|
return selected;
|
|
249
342
|
}
|
|
250
343
|
async function recommendedItems(kind) {
|
|
@@ -597,12 +690,12 @@ function modelSkillInstallTask(root, row, scope) {
|
|
|
597
690
|
...(row.installHint ? [`仓库入口提示:${row.installHint}`] : []),
|
|
598
691
|
`安装位置:${skillScopeLabel(scope)}。`,
|
|
599
692
|
`唯一允许的最终目标目录:${target}`,
|
|
600
|
-
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 data/raw、.git 或研究产物。",
|
|
693
|
+
"用户已通过 Skill 管理页授权本次安装。请使用当前会话的联网、文件和命令工具读取来源仓库,并直接下载、安装所需依赖和完成 Skill 安装;不要再次要求安装权限。不要写入其他 Skill 目录,不要修改 .psyclaw/data/raw、data/raw、.git 或研究产物。",
|
|
601
694
|
row.collection
|
|
602
695
|
? "这是多 Skill 套件:目标目录自身无需 SKILL.md,但其子目录必须包含一个或多个有效 SKILL.md。保留套件内共享目录和相对路径,不得包含 .git、符号链接或凭据。"
|
|
603
696
|
: "目标目录最终必须直接包含有效 SKILL.md(YAML frontmatter 至少包含 name 和 description),不得包含 .git、符号链接、凭据或二进制大文件。",
|
|
604
697
|
"如果仓库包含多个 Skill,只安装与此推荐项相符的部分;如果它不是 Skill 或无法合理适配,停止并说明原因,不要伪造 SKILL.md。",
|
|
605
|
-
"安装完成后检查目标目录结构,并提醒用户执行 /
|
|
698
|
+
"安装完成后检查目标目录结构,并提醒用户执行 /skill 启用该项,再执行 /reload。",
|
|
606
699
|
].join("\n");
|
|
607
700
|
}
|
|
608
701
|
async function chooseSkillScope(ctx) {
|
|
@@ -628,17 +721,17 @@ async function queueModelSkillInstall(pi, ctx, row) {
|
|
|
628
721
|
state.skillScopes = { ...(state.skillScopes ?? {}), [row.id]: scope };
|
|
629
722
|
await saveRecommendationState(ctx.cwd, state);
|
|
630
723
|
pi.sendUserMessage(modelSkillInstallTask(ctx.cwd, row, scope), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
631
|
-
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /
|
|
724
|
+
ctx.ui.notify(`已将 ${row.name} 的安装任务交给当前模型,目标为${skillScopeLabel(scope)}。安装完成后请在 /skill 中启用,再执行 /reload。`, "info");
|
|
632
725
|
}
|
|
633
726
|
async function showSkillManager(pi, args, ctx) {
|
|
634
727
|
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
635
728
|
const verb = action[0];
|
|
636
729
|
const id = action[1];
|
|
637
730
|
if (verb && !["status", "enable", "disable", "enable-all", "disable-all", "install"].includes(verb)) {
|
|
638
|
-
throw new Error("Usage: /
|
|
731
|
+
throw new Error("Usage: /skill [status|enable <id>|disable <id>|enable-all|disable-all]");
|
|
639
732
|
}
|
|
640
733
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
641
|
-
throw new Error(`Usage: /
|
|
734
|
+
throw new Error(`Usage: /skill ${verb} <id>`);
|
|
642
735
|
}
|
|
643
736
|
if (verb === "install") {
|
|
644
737
|
const row = (await skillManagerRows(ctx.cwd, await readRecommendationState(ctx.cwd))).find((candidate) => candidate.id === normalizeRecommendedSkillId(id));
|
|
@@ -858,17 +951,14 @@ export default function psyclawExtension(pi) {
|
|
|
858
951
|
const legacyTestApi = typeof pi.registerTool !== "function";
|
|
859
952
|
const runtimeMcps = new RuntimeMcpRegistry();
|
|
860
953
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
861
|
-
pi.on("resources_discover", async (event
|
|
954
|
+
pi.on("resources_discover", async (event) => {
|
|
862
955
|
const enabled = await enabledRecommendedSkillPaths(event.cwd);
|
|
863
|
-
for (const warning of enabled.warnings)
|
|
864
|
-
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
865
956
|
const local = await enabledLocalSkillPaths(event.cwd, {
|
|
866
957
|
excludedNames: [...coreSkillNames(), ...await skillNamesInPaths(enabled.paths)],
|
|
867
958
|
});
|
|
868
|
-
for (const warning of local.warnings)
|
|
869
|
-
ctx.ui.notify(`PsyClaw Skill: ${warning}`, "warning");
|
|
870
959
|
// Return only existing Skill directories. Same-name local/core candidates
|
|
871
|
-
// are resolved before Pi sees them
|
|
960
|
+
// are resolved before Pi sees them. Discovery warnings belong in /skill;
|
|
961
|
+
// they should not interrupt every startup.
|
|
872
962
|
return {
|
|
873
963
|
skillPaths: [...enabled.paths, ...local.paths],
|
|
874
964
|
promptPaths: await enabledLocalPromptPaths(event.cwd),
|
|
@@ -876,6 +966,47 @@ export default function psyclawExtension(pi) {
|
|
|
876
966
|
});
|
|
877
967
|
if (!legacyTestApi && typeof pi.on === "function")
|
|
878
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
|
+
});
|
|
879
1010
|
pi.registerCommand("init", {
|
|
880
1011
|
description: "初始化可追溯的研究项目",
|
|
881
1012
|
handler: async (args, ctx) => {
|
|
@@ -883,8 +1014,9 @@ export default function psyclawExtension(pi) {
|
|
|
883
1014
|
const parsed = parseInitArgs(args);
|
|
884
1015
|
const project = await bootstrapProject({ root: ctx.cwd, ...parsed });
|
|
885
1016
|
if (typeof pi.sendUserMessage === "function") {
|
|
1017
|
+
pendingInitApprovals.add(ctx.cwd);
|
|
886
1018
|
pi.sendUserMessage(academicGrillRequest(project.goal, "init"), ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
887
|
-
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm}
|
|
1019
|
+
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。学术追问完成并生成主要计划文档后会进入审批;全部批准后才能 /run。`, "info");
|
|
888
1020
|
}
|
|
889
1021
|
else {
|
|
890
1022
|
ctx.ui.notify(`研究项目已初始化:${project.id}(${project.paradigm})。使用 /run 启动受控研究流程。`, "info");
|
|
@@ -895,6 +1027,18 @@ export default function psyclawExtension(pi) {
|
|
|
895
1027
|
}
|
|
896
1028
|
},
|
|
897
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
|
+
});
|
|
898
1042
|
if (!legacyTestApi)
|
|
899
1043
|
pi.registerCommand("run", {
|
|
900
1044
|
description: "启动受控研究流程,可用 --skills a,b 选择本次优化 Skill",
|
|
@@ -919,8 +1063,16 @@ export default function psyclawExtension(pi) {
|
|
|
919
1063
|
}
|
|
920
1064
|
ctx.ui.notify("检测到合规的分析文档,已自动补齐研究项目记录并启动受控流程。", "info");
|
|
921
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
|
+
}
|
|
922
1072
|
const available = await selectableRunSkills(ctx.cwd);
|
|
923
1073
|
let requestedSkills = parsed.requestedSkills;
|
|
1074
|
+
if (parsed.mode === "auto" && requestedSkills === undefined)
|
|
1075
|
+
requestedSkills = [];
|
|
924
1076
|
if (requestedSkills === undefined && ctx.hasUI && typeof ctx.ui.input === "function") {
|
|
925
1077
|
const answer = await ctx.ui.input("选择本次 Run 使用的 Skill(逗号分隔,可留空)", available.map((skill) => skill.name).join(", "));
|
|
926
1078
|
if (answer === undefined) {
|
|
@@ -931,10 +1083,24 @@ export default function psyclawExtension(pi) {
|
|
|
931
1083
|
}
|
|
932
1084
|
const selectedSkills = resolveRunSkills(requestedSkills ?? [], available);
|
|
933
1085
|
const objective = parsed.objective || project.goal;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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");
|
|
938
1104
|
}
|
|
939
1105
|
catch (error) {
|
|
940
1106
|
await notifyError(ctx, error);
|
|
@@ -1072,27 +1238,73 @@ export default function psyclawExtension(pi) {
|
|
|
1072
1238
|
},
|
|
1073
1239
|
});
|
|
1074
1240
|
if (!legacyTestApi)
|
|
1075
|
-
pi.registerCommand("
|
|
1076
|
-
description: "
|
|
1241
|
+
pi.registerCommand("skill", {
|
|
1242
|
+
description: "管理和安装 Skill",
|
|
1077
1243
|
handler: async (args, ctx) => {
|
|
1078
|
-
|
|
1079
|
-
|
|
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;
|
|
1080
1253
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
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;
|
|
1083
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");
|
|
1084
1279
|
},
|
|
1085
1280
|
});
|
|
1086
1281
|
if (!legacyTestApi)
|
|
1087
|
-
pi.registerCommand("
|
|
1088
|
-
description: "
|
|
1282
|
+
pi.registerCommand("plugin", {
|
|
1283
|
+
description: "管理 Plugin / Extension",
|
|
1089
1284
|
handler: async (args, ctx) => {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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);
|
|
1094
1307
|
}
|
|
1095
|
-
pi.sendUserMessage(`/skill:${name}${rest.length ? ` ${rest.join(" ")}` : ""}`, ctx.isIdle() ? {} : { deliverAs: "followUp" });
|
|
1096
1308
|
},
|
|
1097
1309
|
});
|
|
1098
1310
|
if (!legacyTestApi)
|
|
@@ -1118,7 +1330,7 @@ export default function psyclawExtension(pi) {
|
|
|
1118
1330
|
const skillLines = skills.items.slice(0, 8).map((item) => `Skill: ${String(item.id)} — ${String(item.name)}`);
|
|
1119
1331
|
const mcpLines = mcps.items.slice(0, 8).map((item) => `MCP: ${String(item.id)} — ${String(item.name)}`);
|
|
1120
1332
|
const toolLines = skills.externalTools.slice(0, 8).map((item) => `外部工具: ${String(item.name)} — ${String(item.sourceRef ?? "请查看项目文档")}`);
|
|
1121
|
-
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");
|
|
1122
1334
|
return;
|
|
1123
1335
|
}
|
|
1124
1336
|
if (kind !== "skill" && kind !== "mcp")
|
|
@@ -1202,11 +1414,19 @@ export default function psyclawExtension(pi) {
|
|
|
1202
1414
|
selectedId = choice;
|
|
1203
1415
|
}
|
|
1204
1416
|
if (preset && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
1205
|
-
const
|
|
1417
|
+
const credential = await providerCredentialSource(preset);
|
|
1418
|
+
const key = credential === "missing" ? await promptProviderKey(ctx, preset.name, preset.apiKeyEnv) : "";
|
|
1206
1419
|
if (key === undefined)
|
|
1207
1420
|
return;
|
|
1208
1421
|
await saveProviderConfig({ ...preset, ...(key ? { apiKey: key } : {}) });
|
|
1209
|
-
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
|
+
}
|
|
1210
1430
|
models = ctx.modelRegistry.getAll().filter((model) => model.provider === requested);
|
|
1211
1431
|
}
|
|
1212
1432
|
const selected = models.find((model) => model.id === selectedId);
|
|
@@ -1529,7 +1749,10 @@ export default function psyclawExtension(pi) {
|
|
|
1529
1749
|
}
|
|
1530
1750
|
},
|
|
1531
1751
|
});
|
|
1532
|
-
|
|
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)
|
|
1533
1756
|
pi.registerCommand("agents", {
|
|
1534
1757
|
description: "运行经批准的只读研究 Agent",
|
|
1535
1758
|
handler: async (args, ctx) => {
|
|
@@ -1556,7 +1779,7 @@ export default function psyclawExtension(pi) {
|
|
|
1556
1779
|
ctx.ui.notify("请先使用 /init 初始化研究项目,再运行 /agents", "warning");
|
|
1557
1780
|
return;
|
|
1558
1781
|
}
|
|
1559
|
-
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.");
|
|
1560
1783
|
if (!approved) {
|
|
1561
1784
|
ctx.ui.notify("Agent run canceled", "info");
|
|
1562
1785
|
return;
|