psyclaw 0.26.3 → 0.27.0
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 +35 -8
- package/dist/src/adapters/pi/extension.js +274 -119
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/branding.d.ts +9 -0
- package/dist/src/branding.js +23 -0
- package/dist/src/branding.js.map +1 -1
- package/dist/src/chat.d.ts +3 -0
- package/dist/src/chat.js +22 -6
- package/dist/src/chat.js.map +1 -1
- package/dist/src/cli.js +20 -8
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/setup.d.ts +1 -1
- package/dist/src/setup.js +12 -4
- package/dist/src/setup.js.map +1 -1
- package/dist/src/skills/recommended.d.ts +75 -0
- package/dist/src/skills/recommended.js +291 -0
- package/dist/src/skills/recommended.js.map +1 -0
- package/dist/src/style/cli-ui.js +2 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/dist/src/telemetry/export.d.ts +24 -0
- package/dist/src/telemetry/export.js +264 -0
- package/dist/src/telemetry/export.js.map +1 -0
- package/dist/src/tui/skill-manager.d.ts +49 -0
- package/dist/src/tui/skill-manager.js +133 -0
- package/dist/src/tui/skill-manager.js.map +1 -0
- package/dist/src/updates/registry.d.ts +1 -0
- package/dist/src/updates/registry.js +19 -0
- package/dist/src/updates/registry.js.map +1 -1
- package/dist/src/updates/update.d.ts +32 -4
- package/dist/src/updates/update.js +163 -6
- package/dist/src/updates/update.js.map +1 -1
- package/package.json +4 -3
- package/scripts/rebrand-pi.mjs +16 -12
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/manifest.json +15 -15
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
- package/skills/recommended/catalog.json +14 -13
package/README.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
PsyClaw 是面向社会科学研究的 Pi 工作台。它把研究项目、证据来源、Claim-Evidence 账本、完整性门禁、人工裁决、可恢复工作流和本地面板接到官方 Pi runtime 上;它不替代统计软件,也不会把未经核验的引用、结果或审稿意见写成事实。
|
|
4
4
|
|
|
5
|
-
当前版本:`0.
|
|
5
|
+
当前版本:`0.27.0`。本仓库以 `psypi v0.4.1` 的完整工作台为功能基线完成改名;`psypi` 仓库保留为历史仓库,正式命令和后续发布均使用 `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.
|
|
12
|
+
npm install -g psyclaw@0.27.0
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
中国大陆网络较慢或无法访问官方源时:
|
|
16
16
|
|
|
17
17
|
```powershell
|
|
18
|
-
npm install -g psyclaw@0.
|
|
18
|
+
npm install -g psyclaw@0.27.0 --registry=https://registry.npmmirror.com
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
确认命令入口:
|
|
@@ -57,13 +57,23 @@ psyclaw brief
|
|
|
57
57
|
| 创建人工裁决模板 | `psyclaw hitl init` |
|
|
58
58
|
| 写研究移交记录 | `psyclaw handoff` |
|
|
59
59
|
| 扫描本机其他 Agent | `psyclaw agents` |
|
|
60
|
-
| 查看 Pi
|
|
61
|
-
| 更新 Pi
|
|
60
|
+
| 查看 PsyClaw 和内置 Pi 更新 | `psyclaw check-updates` |
|
|
61
|
+
| 更新 PsyClaw 和内置 Pi | `psyclaw update` |
|
|
62
|
+
| 导出脱敏使用路径 | 在对话中输入 `/trace` |
|
|
62
63
|
| 打开科研面板 | 在对话中输入 `/panel` |
|
|
63
64
|
| 查看或切换 Provider | 在对话中输入 `/provider` 或 `/provider <id>` |
|
|
64
65
|
| 管理启动横幅宠物 | `/pet status`、`/pet on`、`/pet off`(默认关闭) |
|
|
65
66
|
|
|
66
|
-
`psyclaw update`
|
|
67
|
+
`psyclaw update` 会更新 PsyClaw 整包,并同时安装该版本锁定、验证过的内置 Pi;它不会更新或删除系统中单独安装的 `pi` 命令。在源码仓库中运行时,命令会停止并提示通过 Git 更新,不会覆盖本地修改。
|
|
68
|
+
仅查看更新计划而不执行时,使用 `psyclaw update --check`。旧的 `--yes` 参数仍兼容,但不再需要。
|
|
69
|
+
|
|
70
|
+
### 导出使用路径
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
/trace
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
该斜杠命令直接在 PsyClaw 对话界面中使用,从当前项目的工作流日志和对应 Pi 会话中生成 OTLP/HTTP JSON,用于通过 OpenTelemetry Collector 导入 Langfuse 或 LangSmith。导出器只保留步骤类型、父子关系、时间、结果状态和版本;不包含对话正文、工具参数/输出、研究内容、原始 ID 或绝对路径。命令只写本地文件,不会自动上传。终端兼容入口仍为 `psyclaw traces export`。
|
|
67
77
|
|
|
68
78
|
## 开发者本地安装
|
|
69
79
|
|
|
@@ -74,7 +84,7 @@ npm link
|
|
|
74
84
|
psyclaw --help
|
|
75
85
|
```
|
|
76
86
|
|
|
77
|
-
开发阶段不需要执行 `psyclaw update
|
|
87
|
+
开发阶段不需要执行 `psyclaw update`。请通过 Git 同步仓库,再运行 `pnpm install` 和 `pnpm build`。
|
|
78
88
|
|
|
79
89
|
## 从旧 Python 版迁移
|
|
80
90
|
|
|
@@ -89,12 +99,29 @@ where.exe psyclaw
|
|
|
89
99
|
|
|
90
100
|
```powershell
|
|
91
101
|
npm uninstall -g psyclaw
|
|
92
|
-
npm install -g psyclaw@0.
|
|
102
|
+
npm install -g psyclaw@0.27.0
|
|
93
103
|
psyclaw --help
|
|
94
104
|
```
|
|
95
105
|
|
|
96
106
|
Python 的卸载器提示 `No files were found to uninstall` 时,不要手工删除不明目录;先用上述命令定位旧入口,再在对应 Python 环境中修复或移除它。
|
|
97
107
|
|
|
108
|
+
## 卸载
|
|
109
|
+
|
|
110
|
+
仅卸载 PsyClaw 程序,保留用户配置和历史会话:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm uninstall -g psyclaw
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
完全卸载 PsyClaw(同时删除模型配置、用户设置和历史会话):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm uninstall -g psyclaw
|
|
120
|
+
node -e 'const fs=require("node:fs"),os=require("node:os"),path=require("node:path");const target=path.join(os.homedir(),`.psy${"pi"}`);if(path.dirname(target)!==os.homedir())throw new Error("unsafe PsyClaw data path");fs.rmSync(target,{recursive:true,force:true});console.log(`removed ${target}`)'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
上述命令不会删除研究项目目录,因为其中可能包含论文、证据、原始数据和分析产物。如需删除某个项目,应单独确认该项目的准确路径后处理。单独全局安装的 `@earendil-works/pi-coding-agent` 不属于 PsyClaw 用户数据,上述命令不会删除它。
|
|
124
|
+
|
|
98
125
|
## 文档
|
|
99
126
|
|
|
100
127
|
- [PsyClaw v0.26.0 使用白皮书](docs/PsyClaw使用白皮书_v0.26.0.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { asProject, bootstrapProject, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, publishManuscript, recordCitationUse, writeHandoff } from "../../index.js";
|
|
3
|
+
import { asProject, bootstrapProject, exportTraces, projectPaths, runOfflineBrief, runInstitutionalFulltext, runLiteratureReview, runExpertReview, runAnalysisDelegation, runWritingReview, runMetaAnalysis, createStageRunner, publishManuscript, recordCitationUse, 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";
|
|
@@ -10,6 +10,8 @@ import { mkdir, readFile } from "node:fs/promises";
|
|
|
10
10
|
import { PROVIDER_PRESETS } from "../../setup.js";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { coreSkillNames, installRecommendedSkill, normalizeRecommendedSkillId, readRecommendationState, readRecommendedCatalog, saveRecommendationState, validateInstalledRecommendedSkill, } from "../../skills/recommended.js";
|
|
14
|
+
import { SkillManagerComponent } from "../../tui/skill-manager.js";
|
|
13
15
|
const PARADIGMS = new Set([
|
|
14
16
|
"survey-observational",
|
|
15
17
|
"qualitative-thematic",
|
|
@@ -44,19 +46,6 @@ async function notifyError(ctx, error) {
|
|
|
44
46
|
}
|
|
45
47
|
const activeAgentRuns = new Set();
|
|
46
48
|
const CORE_SKILLS = new Set(["research-intake", "evidence-capture", "citation-audit", "research-brief"]);
|
|
47
|
-
async function recommendationState(root) {
|
|
48
|
-
try {
|
|
49
|
-
const value = JSON.parse(await readFile(join(root, ".psyclaw", "recommendations.json"), "utf8"));
|
|
50
|
-
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: Array.isArray(value.skills) ? value.skills.filter((id) => typeof id === "string") : [], mcp: Array.isArray(value.mcp) ? value.mcp.filter((id) => typeof id === "string") : [] };
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: [], mcp: [] };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function saveRecommendationState(root, state) {
|
|
57
|
-
await mkdir(join(root, ".psyclaw"), { recursive: true });
|
|
58
|
-
await atomicWriteFile(join(root, ".psyclaw", "recommendations.json"), `${JSON.stringify({ ...state, skills: [...new Set(state.skills)].sort(), mcp: [...new Set(state.mcp)].sort() }, null, 2)}\n`);
|
|
59
|
-
}
|
|
60
49
|
function coreSkillPath(name) {
|
|
61
50
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "skills", "core", name, "SKILL.md");
|
|
62
51
|
}
|
|
@@ -154,118 +143,257 @@ async function recommendedItems(kind) {
|
|
|
154
143
|
}
|
|
155
144
|
return { items: [], installPrep: [] };
|
|
156
145
|
}
|
|
157
|
-
async function
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return;
|
|
146
|
+
async function skillManagerRows(root, state) {
|
|
147
|
+
const catalog = await readRecommendedCatalog();
|
|
148
|
+
return Promise.all(catalog.items.filter((item) => item.kind === "skill").map(async (item) => {
|
|
149
|
+
const id = normalizeRecommendedSkillId(item.id);
|
|
150
|
+
try {
|
|
151
|
+
await validateInstalledRecommendedSkill(root, id);
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
name: item.name,
|
|
155
|
+
description: String(item.description ?? ""),
|
|
156
|
+
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
157
|
+
installed: true,
|
|
158
|
+
enabled: state.skills.includes(id),
|
|
159
|
+
blocked: false,
|
|
160
|
+
};
|
|
173
161
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const selectedItem = items[selectedIndex];
|
|
190
|
-
if (!selectedItem)
|
|
191
|
-
return;
|
|
192
|
-
const targetId = String(selectedItem.id);
|
|
193
|
-
const isCurrentlyEnabled = enabled.has(targetId);
|
|
194
|
-
const actionChoice = await ctx.ui.select(`管理推荐 ${kind === "skills" ? "Skill" : "MCP"}: ${String(selectedItem.name)} (${targetId})`, [
|
|
195
|
-
isCurrentlyEnabled ? `🔴 禁用 (Disable ${String(selectedItem.name)})` : `🟢 启用 (Enable ${String(selectedItem.name)})`,
|
|
196
|
-
`📦 查看安装预检与依赖计划 (Install Plan)`,
|
|
197
|
-
`🌐 查看来源仓库 (${String(selectedItem.sourceRef ?? "无")})`,
|
|
198
|
-
`↩️ 取消`,
|
|
199
|
-
]);
|
|
200
|
-
if (!actionChoice || actionChoice === "↩️ 取消")
|
|
201
|
-
return;
|
|
202
|
-
if (actionChoice.startsWith("🔴") || actionChoice.startsWith("🟢")) {
|
|
203
|
-
const nextAction = isCurrentlyEnabled ? "disable" : "enable";
|
|
204
|
-
if (await ctx.ui.confirm(`${nextAction === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(selectedItem.name)} 的状态将写入当前项目配置。`)) {
|
|
205
|
-
const current = new Set(state[stateField]);
|
|
206
|
-
if (nextAction === "enable")
|
|
207
|
-
current.add(targetId);
|
|
208
|
-
else
|
|
209
|
-
current.delete(targetId);
|
|
210
|
-
state[stateField] = [...current];
|
|
211
|
-
await saveRecommendationState(ctx.cwd, state);
|
|
212
|
-
ctx.ui.notify(`${nextAction === "enable" ? "已启用" : "已禁用"} ${String(selectedItem.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
162
|
+
catch (error) {
|
|
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);
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
name: item.name,
|
|
170
|
+
description: String(item.description ?? ""),
|
|
171
|
+
...(item.sourceRef === undefined ? {} : { sourceRef: item.sourceRef }),
|
|
172
|
+
installed: false,
|
|
173
|
+
enabled: false,
|
|
174
|
+
blocked: !installable,
|
|
175
|
+
reason: installable ? "尚未安装" : (plan?.blockedReason ?? (error instanceof Error ? error.message : String(error))),
|
|
176
|
+
};
|
|
215
177
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
async function mcpManagerRows(state) {
|
|
181
|
+
const { items, installPrep } = await recommendedItems("mcp");
|
|
182
|
+
return items.map((item) => {
|
|
183
|
+
const id = String(item.id ?? "");
|
|
184
|
+
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
185
|
+
const blocked = plan?.status === "blocked";
|
|
186
|
+
const dependencies = Array.isArray(plan?.dependencies)
|
|
187
|
+
? plan.dependencies.filter((value) => typeof value === "string")
|
|
188
|
+
: [];
|
|
189
|
+
return {
|
|
190
|
+
id,
|
|
191
|
+
name: String(item.name ?? id),
|
|
192
|
+
description: String(item.description ?? ""),
|
|
193
|
+
...(typeof item.sourceRef === "string" ? { sourceRef: item.sourceRef } : {}),
|
|
194
|
+
enabled: state.mcp.includes(id),
|
|
195
|
+
blocked,
|
|
196
|
+
...(typeof plan?.blockedReason === "string" ? { reason: plan.blockedReason } : {}),
|
|
197
|
+
details: [
|
|
198
|
+
`传输:${String(item.transport ?? "未声明")} · 风险:${String(item.risk ?? "未声明")}`,
|
|
199
|
+
`版本:${String(plan?.ref ?? "未固定")} · 许可证:${String(plan?.license ?? "未核验")}`,
|
|
200
|
+
`依赖:${dependencies.length > 0 ? dependencies.join(", ") : "无已声明依赖"}`,
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function skillRowLabel(row) {
|
|
206
|
+
const state = row.blocked ? "× 阻断" : row.enabled ? "● 已启用" : row.installed ? "○ 未启用" : "↓ 未安装";
|
|
207
|
+
return `${state} ${row.name} [${row.id}]`;
|
|
208
|
+
}
|
|
209
|
+
async function setRecommendedSkillEnabled(root, requestedId, enabled) {
|
|
210
|
+
const id = normalizeRecommendedSkillId(requestedId);
|
|
211
|
+
if (enabled)
|
|
212
|
+
await validateInstalledRecommendedSkill(root, id);
|
|
213
|
+
const state = await readRecommendationState(root);
|
|
214
|
+
const current = new Set(state.skills);
|
|
215
|
+
if (enabled)
|
|
216
|
+
current.add(id);
|
|
217
|
+
else
|
|
218
|
+
current.delete(id);
|
|
219
|
+
state.skills = [...current];
|
|
220
|
+
await saveRecommendationState(root, state);
|
|
221
|
+
}
|
|
222
|
+
function skillManagerItems(rows) {
|
|
223
|
+
return [
|
|
224
|
+
...coreSkillNames().map((name) => ({
|
|
225
|
+
id: `core:${name}`,
|
|
226
|
+
name,
|
|
227
|
+
description: "PsyClaw 核心研究流程 Skill。",
|
|
228
|
+
status: "core",
|
|
229
|
+
sourceRef: "PsyClaw core",
|
|
230
|
+
})),
|
|
231
|
+
...rows.map((row) => ({
|
|
232
|
+
id: row.id,
|
|
233
|
+
name: row.name,
|
|
234
|
+
description: row.description,
|
|
235
|
+
status: row.blocked ? "blocked" : row.enabled ? "enabled" : row.installed ? "disabled" : "missing",
|
|
236
|
+
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
237
|
+
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
238
|
+
})),
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
async function openSkillManager(ctx, rows) {
|
|
242
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(skillManagerItems(rows), tui, theme, keybindings, done)));
|
|
243
|
+
}
|
|
244
|
+
function mcpManagerItems(rows) {
|
|
245
|
+
return rows.map((row) => ({
|
|
246
|
+
id: row.id,
|
|
247
|
+
name: row.name,
|
|
248
|
+
description: row.description,
|
|
249
|
+
status: row.blocked ? "blocked" : row.enabled ? "enabled" : "disabled",
|
|
250
|
+
configuredEnabled: row.enabled,
|
|
251
|
+
...(row.sourceRef === undefined ? {} : { sourceRef: row.sourceRef }),
|
|
252
|
+
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
253
|
+
details: row.details,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
async function openMcpManager(ctx, rows) {
|
|
257
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => (new SkillManagerComponent(mcpManagerItems(rows), tui, theme, keybindings, done, {
|
|
258
|
+
title: "MCP 管理",
|
|
259
|
+
itemLabel: "MCP",
|
|
260
|
+
footer: "↑/↓ 移动 · Space 开启/关闭项目配置 · Enter 查看预检 · Esc 关闭",
|
|
261
|
+
enterAction: "install",
|
|
262
|
+
enabledText: "项目配置已开启;运行时仍需通过安装、信任和工具策略检查",
|
|
263
|
+
disabledText: "项目配置已关闭",
|
|
264
|
+
blockedText: "预检阻断;若配置曾开启,可按 Space 关闭",
|
|
265
|
+
})));
|
|
266
|
+
}
|
|
267
|
+
async function setRecommendedMcpEnabled(root, id, enabled) {
|
|
268
|
+
const state = await readRecommendationState(root);
|
|
269
|
+
const rows = await mcpManagerRows(state);
|
|
270
|
+
const row = rows.find((candidate) => candidate.id === id);
|
|
271
|
+
if (!row)
|
|
272
|
+
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
273
|
+
if (enabled && row.blocked)
|
|
274
|
+
throw new Error(row.reason ?? `MCP 预检未通过: ${id}`);
|
|
275
|
+
const current = new Set(state.mcp);
|
|
276
|
+
if (enabled)
|
|
277
|
+
current.add(id);
|
|
278
|
+
else
|
|
279
|
+
current.delete(id);
|
|
280
|
+
state.mcp = [...current];
|
|
281
|
+
await saveRecommendationState(root, state);
|
|
282
|
+
}
|
|
283
|
+
function mcpInstallPlan(id, items, installPrep) {
|
|
284
|
+
const item = items.find((candidate) => candidate.id === id);
|
|
285
|
+
if (!item)
|
|
286
|
+
throw new Error(`未找到推荐 MCP: ${id}`);
|
|
287
|
+
const plan = installPrep.find((candidate) => candidate.id === id);
|
|
288
|
+
return {
|
|
289
|
+
schemaVersion: "psyclaw/recommendation-install/v1",
|
|
290
|
+
kind: "mcp",
|
|
291
|
+
id,
|
|
292
|
+
name: item.name,
|
|
293
|
+
sourceRef: item.sourceRef,
|
|
294
|
+
plan: plan ?? null,
|
|
295
|
+
status: plan?.status ?? "review-required",
|
|
296
|
+
next: "这里只展示固定来源、版本、许可证和依赖预检;完成受信任安装与注册后,MCP 才能在运行时调用。",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function showMcpManager(args, ctx) {
|
|
300
|
+
const [verb, id] = args.trim().split(/\s+/).filter(Boolean);
|
|
301
|
+
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
302
|
+
throw new Error("Usage: /mcp [status|enable <id>|disable <id>|install <id>]");
|
|
303
|
+
}
|
|
304
|
+
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
305
|
+
throw new Error(`Usage: /mcp ${verb} <id>`);
|
|
306
|
+
}
|
|
307
|
+
const catalog = await recommendedItems("mcp");
|
|
308
|
+
if (verb === "install") {
|
|
309
|
+
ctx.ui.notify(JSON.stringify(mcpInstallPlan(id, catalog.items, catalog.installPrep), null, 2), "info");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (verb === "enable" || verb === "disable") {
|
|
313
|
+
await setRecommendedMcpEnabled(ctx.cwd, id, verb === "enable");
|
|
314
|
+
ctx.ui.notify(`MCP ${id} 的项目配置已${verb === "enable" ? "开启" : "关闭"};重启后重新检查安装、信任和工具策略`, "info");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
318
|
+
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
319
|
+
ctx.ui.notify(rows.map((row) => `${row.blocked ? "[!]" : row.enabled ? "[on]" : "[off]"} ${row.id} — ${row.name}${row.reason ? ` — ${row.reason}` : ""}`).join("\n"), "info");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
while (true) {
|
|
323
|
+
const rows = await mcpManagerRows(await readRecommendationState(ctx.cwd));
|
|
324
|
+
const action = await openMcpManager(ctx, rows);
|
|
325
|
+
if (action.type === "close")
|
|
228
326
|
return;
|
|
327
|
+
const row = rows.find((candidate) => candidate.id === action.id);
|
|
328
|
+
if (!row)
|
|
329
|
+
continue;
|
|
330
|
+
if (action.type === "install") {
|
|
331
|
+
ctx.ui.notify(JSON.stringify(mcpInstallPlan(row.id, catalog.items, catalog.installPrep), null, 2), "info");
|
|
332
|
+
continue;
|
|
229
333
|
}
|
|
230
|
-
if (
|
|
231
|
-
ctx.
|
|
232
|
-
|
|
334
|
+
if (action.type === "toggle") {
|
|
335
|
+
await setRecommendedMcpEnabled(ctx.cwd, row.id, action.enabled);
|
|
336
|
+
ctx.ui.notify(`${row.name} 的项目配置已${action.enabled ? "开启" : "关闭"};重启后重新检查运行时可用性`, "info");
|
|
233
337
|
}
|
|
234
|
-
return;
|
|
235
338
|
}
|
|
236
|
-
|
|
339
|
+
}
|
|
340
|
+
async function showSkillManager(args, ctx) {
|
|
341
|
+
const action = args.trim().split(/\s+/).filter(Boolean);
|
|
342
|
+
const verb = action[0];
|
|
343
|
+
const id = action[1];
|
|
344
|
+
if (verb && !["status", "enable", "disable", "install"].includes(verb)) {
|
|
345
|
+
throw new Error("Usage: /skills [status|enable <id>|disable <id>|install <id>]");
|
|
346
|
+
}
|
|
237
347
|
if ((verb === "enable" || verb === "disable" || verb === "install") && !id) {
|
|
238
|
-
throw new Error(`Usage:
|
|
348
|
+
throw new Error(`Usage: /skills ${verb} <id>`);
|
|
349
|
+
}
|
|
350
|
+
if (verb === "install") {
|
|
351
|
+
const result = await installRecommendedSkill(ctx.cwd, id);
|
|
352
|
+
ctx.ui.notify(`${result.installed ? "已安装" : "已经安装"} ${result.skillName}\n${result.path}\n安装不会自动启用,请返回 /skills 开启。`, "info");
|
|
353
|
+
return;
|
|
239
354
|
}
|
|
240
355
|
if (verb === "enable" || verb === "disable") {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
throw new Error(`未找到推荐${stateKey === "skill" ? " Skill" : " MCP"}: ${id}`);
|
|
244
|
-
if (!ctx.hasUI || !(await ctx.ui.confirm(`${verb === "enable" ? "启用" : "禁用"}推荐${stateKey === "skill" ? " Skill" : " MCP"}`, `${String(item.name)} 的状态将写入当前项目配置。`)))
|
|
245
|
-
return;
|
|
246
|
-
const current = new Set(state[stateField]);
|
|
247
|
-
if (verb === "enable")
|
|
248
|
-
current.add(id);
|
|
249
|
-
else
|
|
250
|
-
current.delete(id);
|
|
251
|
-
state[stateField] = [...current];
|
|
252
|
-
await saveRecommendationState(ctx.cwd, state);
|
|
253
|
-
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已禁用"} ${String(item.name)}(/reload 后生效;打开 /panel 可可视化管理)`, "info");
|
|
356
|
+
await setRecommendedSkillEnabled(ctx.cwd, id, verb === "enable");
|
|
357
|
+
ctx.ui.notify(`${verb === "enable" ? "已启用" : "已停用"} ${normalizeRecommendedSkillId(id)},下次启动 PsyClaw 生效`, "info");
|
|
254
358
|
return;
|
|
255
359
|
}
|
|
256
|
-
if (verb === "
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
360
|
+
if (!ctx.hasUI || typeof ctx.ui.custom !== "function" || verb === "status") {
|
|
361
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
362
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
363
|
+
const lines = [
|
|
364
|
+
...coreSkillNames().map((name) => `● 内置 ${name}`),
|
|
365
|
+
...rows.map((row) => `${skillRowLabel(row)}${row.reason ? ` — ${row.reason}` : ""}`),
|
|
366
|
+
];
|
|
367
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
262
368
|
return;
|
|
263
369
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
370
|
+
while (true) {
|
|
371
|
+
const state = await readRecommendationState(ctx.cwd);
|
|
372
|
+
const rows = await skillManagerRows(ctx.cwd, state);
|
|
373
|
+
const action = await openSkillManager(ctx, rows);
|
|
374
|
+
if (action.type === "close")
|
|
375
|
+
return;
|
|
376
|
+
const row = rows.find((candidate) => candidate.id === action.id);
|
|
377
|
+
if (!row)
|
|
378
|
+
continue;
|
|
379
|
+
if (action.type === "install") {
|
|
380
|
+
const approved = await ctx.ui.confirm("安装推荐 Skill?", `${row.name}\n固定来源:${row.sourceRef ?? row.id}\n安装到当前项目,安装完成后仍由你决定是否启用。`);
|
|
381
|
+
if (!approved)
|
|
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;
|
|
391
|
+
}
|
|
392
|
+
if (action.type === "toggle") {
|
|
393
|
+
await setRecommendedSkillEnabled(ctx.cwd, row.id, action.enabled);
|
|
394
|
+
ctx.ui.notify(`已${action.enabled ? "启用" : "停用"} ${row.name},重启 PsyClaw 后生效`, "info");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
269
397
|
}
|
|
270
398
|
/** Run meta-analysis with the staged progress captured into a buffer (no TTY). */
|
|
271
399
|
async function runMetaAnalysisStaged(root, target, nStudies) {
|
|
@@ -417,10 +545,10 @@ export default function psyclawExtension(pi) {
|
|
|
417
545
|
});
|
|
418
546
|
if (!legacyTestApi)
|
|
419
547
|
pi.registerCommand("skills", {
|
|
420
|
-
description: "
|
|
548
|
+
description: "打开 Skill 安装与启用管理页",
|
|
421
549
|
handler: async (args, ctx) => {
|
|
422
550
|
try {
|
|
423
|
-
await
|
|
551
|
+
await showSkillManager(args, ctx);
|
|
424
552
|
}
|
|
425
553
|
catch (error) {
|
|
426
554
|
await notifyError(ctx, error);
|
|
@@ -429,10 +557,10 @@ export default function psyclawExtension(pi) {
|
|
|
429
557
|
});
|
|
430
558
|
if (!legacyTestApi)
|
|
431
559
|
pi.registerCommand("mcp", {
|
|
432
|
-
description: "
|
|
560
|
+
description: "打开 MCP 配置与预检管理页",
|
|
433
561
|
handler: async (args, ctx) => {
|
|
434
562
|
try {
|
|
435
|
-
await
|
|
563
|
+
await showMcpManager(args, ctx);
|
|
436
564
|
}
|
|
437
565
|
catch (error) {
|
|
438
566
|
await notifyError(ctx, error);
|
|
@@ -455,10 +583,16 @@ export default function psyclawExtension(pi) {
|
|
|
455
583
|
if (kind !== "skill" && kind !== "mcp")
|
|
456
584
|
throw new Error("Usage: /install skill|mcp <id>");
|
|
457
585
|
if (!id) {
|
|
458
|
-
|
|
586
|
+
if (kind === "skill")
|
|
587
|
+
await showSkillManager("", ctx);
|
|
588
|
+
else
|
|
589
|
+
await showMcpManager("", ctx);
|
|
459
590
|
return;
|
|
460
591
|
}
|
|
461
|
-
|
|
592
|
+
if (kind === "skill")
|
|
593
|
+
await showSkillManager(`install ${id}`, ctx);
|
|
594
|
+
else
|
|
595
|
+
await showMcpManager(`install ${id}`, ctx);
|
|
462
596
|
}
|
|
463
597
|
catch (error) {
|
|
464
598
|
await notifyError(ctx, error);
|
|
@@ -529,6 +663,27 @@ export default function psyclawExtension(pi) {
|
|
|
529
663
|
ctx.ui.notify(`启动横幅宠物已${action === "on" ? "开启" : "关闭"},下次启动生效`, "info");
|
|
530
664
|
},
|
|
531
665
|
});
|
|
666
|
+
const traceCommand = {
|
|
667
|
+
description: "导出脱敏使用路径,供 Langfuse 或 LangSmith 分析",
|
|
668
|
+
handler: async (args, ctx) => {
|
|
669
|
+
try {
|
|
670
|
+
if (args.trim())
|
|
671
|
+
throw new Error("Usage: /trace");
|
|
672
|
+
const result = await exportTraces({ root: ctx.cwd });
|
|
673
|
+
ctx.ui.notify([
|
|
674
|
+
"使用路径已导出(未上传)",
|
|
675
|
+
`文件:${result.output}`,
|
|
676
|
+
`轨迹:${result.traces}`,
|
|
677
|
+
`步骤:${result.spans}`,
|
|
678
|
+
"隐私:不含对话正文、工具参数、原始 ID 或绝对路径",
|
|
679
|
+
].join("\n"), "info");
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
await notifyError(ctx, error);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
pi.registerCommand("trace", traceCommand);
|
|
532
687
|
if (typeof pi.registerTool === "function") {
|
|
533
688
|
pi.registerTool({
|
|
534
689
|
name: "psyclaw_skill",
|