@unifan/pi-unifan-zh 1.0.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 +111 -0
- package/extensions/review/CHANGELOG.md +540 -0
- package/extensions/review/LICENSE +15 -0
- package/extensions/review/README.md +55 -0
- package/extensions/review/agents/bugbot.md +46 -0
- package/extensions/review/agents/claude-md-compliance.md +46 -0
- package/extensions/review/agents/code-comments.md +43 -0
- package/extensions/review/agents/conventions.md +41 -0
- package/extensions/review/agents/gate.md +70 -0
- package/extensions/review/agents/history-context.md +45 -0
- package/extensions/review/agents/lite-review.md +51 -0
- package/extensions/review/agents/security-review.md +45 -0
- package/extensions/review/index.ts +205 -0
- package/extensions/review/package.json +38 -0
- package/extensions/review/reference/README.md +20 -0
- package/extensions/review/reference/claude-code-review.md +133 -0
- package/extensions/review/reference/cursor-review-skills.md +72 -0
- package/extensions/review/reference/pi-review-roadmap.md +183 -0
- package/extensions/review/reference/structured-output.md +26 -0
- package/extensions/review/reference/v0.2-plan.md +268 -0
- package/extensions/review/src/cli-args.ts +105 -0
- package/extensions/review/src/config.ts +340 -0
- package/extensions/review/src/directive.ts +481 -0
- package/extensions/review/src/gate-enforce.ts +151 -0
- package/extensions/review/src/lean-agents.ts +105 -0
- package/extensions/review/src/pr-ref.ts +39 -0
- package/extensions/review/src/report-tool.ts +394 -0
- package/extensions/review/src/report.ts +399 -0
- package/extensions/review/src/review-report.ts +279 -0
- package/extensions/review/src/review-run.ts +568 -0
- package/extensions/review/src/target-workspace.ts +239 -0
- package/extensions/review/src/tool-wrapper.ts +75 -0
- package/extensions/review/src/tui-renderer.ts +92 -0
- package/extensions/review/src/types.ts +207 -0
- package/extensions/review/src/workflow-schemas.ts +172 -0
- package/extensions/sessions/README.md +41 -0
- package/extensions/sessions/index.ts +862 -0
- package/extensions/sessions/package.json +21 -0
- package/extensions/sessions/sessions.ts +264 -0
- package/package.json +36 -0
- package/skills/README.md +29 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unifan/pi-sessions-zh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pi 历史会话管理器(双栏可视化 TUI 预览与恢复,中文增强版)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./index.ts"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-extension",
|
|
14
|
+
"pi-sessions",
|
|
15
|
+
"chinese",
|
|
16
|
+
"tui"
|
|
17
|
+
],
|
|
18
|
+
"author": "unifan",
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export interface SessionInfoLike {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
modified: Date;
|
|
6
|
+
created?: Date;
|
|
7
|
+
firstMessage: string;
|
|
8
|
+
path: string;
|
|
9
|
+
messageCount?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PreviewContent =
|
|
13
|
+
| string
|
|
14
|
+
| Array<
|
|
15
|
+
| { type: "text"; text: string }
|
|
16
|
+
| { type: "image"; mimeType?: string }
|
|
17
|
+
| { type: "toolCall"; name: string; arguments?: Record<string, unknown> }
|
|
18
|
+
| { type: "thinking"; thinking?: string; redacted?: boolean }
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export interface PreviewMessageLike {
|
|
22
|
+
role: string;
|
|
23
|
+
content?: PreviewContent;
|
|
24
|
+
toolName?: string;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
command?: string;
|
|
27
|
+
output?: string;
|
|
28
|
+
summary?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PreviewBlock =
|
|
32
|
+
| { kind: "notice"; text: string }
|
|
33
|
+
| { kind: "user"; text: string }
|
|
34
|
+
| { kind: "assistant"; text: string }
|
|
35
|
+
| { kind: "thinking"; text: string; redacted?: boolean }
|
|
36
|
+
| { kind: "toolCall"; name: string; args?: string }
|
|
37
|
+
| { kind: "toolResult"; name?: string; text: string; isError?: boolean }
|
|
38
|
+
| { kind: "bash"; command: string; output?: string; isError?: boolean }
|
|
39
|
+
| { kind: "summary"; label: string; text: string }
|
|
40
|
+
| { kind: "custom"; label: string; text: string };
|
|
41
|
+
|
|
42
|
+
export interface SessionPreview {
|
|
43
|
+
title: string;
|
|
44
|
+
subtitle: string;
|
|
45
|
+
blocks: PreviewBlock[];
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SessionPaneLayout {
|
|
50
|
+
mode: "single" | "split";
|
|
51
|
+
listWidth: number;
|
|
52
|
+
previewWidth: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseLimit(args: string | undefined, defaultLimit = 5): number {
|
|
56
|
+
if (!args) return defaultLimit;
|
|
57
|
+
const parsed = Number.parseInt(args.trim(), 10);
|
|
58
|
+
if (!Number.isFinite(parsed) || parsed < 1) return defaultLimit;
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pad = (value: number): string => value.toString().padStart(2, "0");
|
|
63
|
+
|
|
64
|
+
export function formatTimestamp(date: Date): string {
|
|
65
|
+
const year = date.getFullYear();
|
|
66
|
+
const month = pad(date.getMonth() + 1);
|
|
67
|
+
const day = pad(date.getDate());
|
|
68
|
+
const hours = pad(date.getHours());
|
|
69
|
+
const minutes = pad(date.getMinutes());
|
|
70
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatRelativeTime(date: Date): string {
|
|
74
|
+
const now = new Date();
|
|
75
|
+
const diffMs = now.getTime() - date.getTime();
|
|
76
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
77
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
78
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
79
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
80
|
+
|
|
81
|
+
if (diffSec < 60) return "刚刚";
|
|
82
|
+
if (diffMin < 60) return `${diffMin}分钟前`;
|
|
83
|
+
if (diffHour < 24) return `${diffHour}小时前`;
|
|
84
|
+
if (diffDay === 1) return "昨天";
|
|
85
|
+
return `${diffDay}天前`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildSessionLabel(session: SessionInfoLike): string {
|
|
89
|
+
const trimmedName = session.name?.trim();
|
|
90
|
+
if (trimmedName) return trimmedName;
|
|
91
|
+
return session.id.length > 8 ? session.id.slice(0, 8) : session.id;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const normalizeSnippet = (text: string, maxLength: number): string => {
|
|
95
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
96
|
+
const fallback = cleaned.length > 0 ? cleaned : "无消息记录";
|
|
97
|
+
if (maxLength < 1) return "";
|
|
98
|
+
if (fallback.length <= maxLength) return fallback;
|
|
99
|
+
if (maxLength === 1) return "…";
|
|
100
|
+
return `${fallback.slice(0, maxLength - 1)}…`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function buildSessionDescription(session: SessionInfoLike, snippetMax = 60): string {
|
|
104
|
+
const snippet = normalizeSnippet(session.firstMessage ?? "", snippetMax);
|
|
105
|
+
return `${formatTimestamp(session.modified)} • ${snippet} — ${session.cwd}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SessionSearchEntry {
|
|
109
|
+
session: SessionInfoLike;
|
|
110
|
+
searchText: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const buildSearchText = (session: SessionInfoLike): string =>
|
|
114
|
+
[session.name?.trim() ?? "", session.id, session.cwd, session.firstMessage ?? ""]
|
|
115
|
+
.join(" ")
|
|
116
|
+
.toLowerCase();
|
|
117
|
+
|
|
118
|
+
export function buildSessionSearchEntries(sessions: SessionInfoLike[]): SessionSearchEntry[] {
|
|
119
|
+
return sessions.map((session) => ({ session, searchText: buildSearchText(session) }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function filterSessionEntries(entries: SessionSearchEntry[], filter: string): SessionSearchEntry[] {
|
|
123
|
+
const normalized = filter.trim().toLowerCase();
|
|
124
|
+
if (!normalized) return entries;
|
|
125
|
+
|
|
126
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
127
|
+
if (tokens.length === 0) return entries;
|
|
128
|
+
|
|
129
|
+
return entries.filter((entry) => tokens.every((token) => entry.searchText.includes(token)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function filterSessionInfos(sessions: SessionInfoLike[], filter: string): SessionInfoLike[] {
|
|
133
|
+
return filterSessionEntries(buildSessionSearchEntries(sessions), filter).map((entry) => entry.session);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getSessionPaneLayout(width: number): SessionPaneLayout {
|
|
137
|
+
if (width < 80) {
|
|
138
|
+
return { mode: "single", listWidth: width, previewWidth: 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dividerWidth = 3;
|
|
142
|
+
const available = Math.max(0, width - dividerWidth);
|
|
143
|
+
let listRatio = 0.36;
|
|
144
|
+
if (width < 110) {
|
|
145
|
+
listRatio = 0.42;
|
|
146
|
+
} else if (width >= 180) {
|
|
147
|
+
listRatio = 0.32;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const listWidth = Math.max(34, Math.min(58, Math.floor(available * listRatio)));
|
|
151
|
+
return {
|
|
152
|
+
mode: "split",
|
|
153
|
+
listWidth,
|
|
154
|
+
previewWidth: Math.max(0, available - listWidth),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cleanPreviewText = (text: string): string => text.replace(/\s+$/g, "");
|
|
159
|
+
|
|
160
|
+
function textBlocksFromContent(content: PreviewContent | undefined): PreviewBlock[] {
|
|
161
|
+
if (!content) return [];
|
|
162
|
+
if (typeof content === "string") return content.trim() ? [{ kind: "assistant", text: cleanPreviewText(content) }] : [];
|
|
163
|
+
|
|
164
|
+
const blocks: PreviewBlock[] = [];
|
|
165
|
+
for (const part of content) {
|
|
166
|
+
if (part.type === "text" && part.text.trim()) {
|
|
167
|
+
blocks.push({ kind: "assistant", text: cleanPreviewText(part.text) });
|
|
168
|
+
} else if (part.type === "image") {
|
|
169
|
+
blocks.push({ kind: "notice", text: `[图片${part.mimeType ? `: ${part.mimeType}` : ""}]` });
|
|
170
|
+
} else if (part.type === "toolCall") {
|
|
171
|
+
const args = part.arguments && Object.keys(part.arguments).length > 0 ? JSON.stringify(part.arguments) : undefined;
|
|
172
|
+
blocks.push({ kind: "toolCall", name: part.name, args });
|
|
173
|
+
} else if (part.type === "thinking") {
|
|
174
|
+
blocks.push({
|
|
175
|
+
kind: "thinking",
|
|
176
|
+
text: part.redacted ? "[思考过程已脱敏]" : cleanPreviewText(part.thinking ?? "[思考过程]"),
|
|
177
|
+
redacted: part.redacted,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return blocks;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function contentToPlainText(content: PreviewContent | undefined): string {
|
|
185
|
+
if (!content) return "";
|
|
186
|
+
if (typeof content === "string") return content;
|
|
187
|
+
return content
|
|
188
|
+
.map((part) => {
|
|
189
|
+
if (part.type === "text") return part.text;
|
|
190
|
+
if (part.type === "image") return `[图片${part.mimeType ? `: ${part.mimeType}` : ""}]`;
|
|
191
|
+
if (part.type === "toolCall") return `[工具调用: ${part.name}]`;
|
|
192
|
+
if (part.type === "thinking") return part.redacted ? "[思考过程已脱敏]" : (part.thinking ?? "[思考过程]");
|
|
193
|
+
return "";
|
|
194
|
+
})
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function messageToBlocks(message: PreviewMessageLike): PreviewBlock[] {
|
|
200
|
+
if (message.role === "user") {
|
|
201
|
+
const text = cleanPreviewText(contentToPlainText(message.content));
|
|
202
|
+
return text ? [{ kind: "user", text }] : [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (message.role === "assistant") {
|
|
206
|
+
return textBlocksFromContent(message.content);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (message.role === "toolResult" || message.role === "tool") {
|
|
210
|
+
const text = cleanPreviewText(contentToPlainText(message.content));
|
|
211
|
+
return text ? [{ kind: "toolResult", name: message.toolName, text, isError: message.isError }] : [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (message.role === "bashExecution") {
|
|
215
|
+
const command = message.command ?? "";
|
|
216
|
+
return command || message.output
|
|
217
|
+
? [{ kind: "bash", command, output: cleanPreviewText(message.output ?? ""), isError: message.isError }]
|
|
218
|
+
: [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (message.role === "compactionSummary" || message.role === "branchSummary") {
|
|
222
|
+
const text = cleanPreviewText(message.summary ?? contentToPlainText(message.content));
|
|
223
|
+
return text ? [{ kind: "summary", label: message.role === "branchSummary" ? "分支摘要" : "对话压缩摘要", text }] : [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const text = cleanPreviewText(message.summary ?? contentToPlainText(message.content));
|
|
227
|
+
return text ? [{ kind: "custom", label: message.role || "消息", text }] : [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildSessionPreview(
|
|
231
|
+
session: SessionInfoLike,
|
|
232
|
+
messages: PreviewMessageLike[],
|
|
233
|
+
options: { maxMessages?: number } = {},
|
|
234
|
+
): SessionPreview {
|
|
235
|
+
const maxMessages = options.maxMessages ?? 80;
|
|
236
|
+
const blocks: PreviewBlock[] = [];
|
|
237
|
+
const omitted = Math.max(0, messages.length - maxMessages);
|
|
238
|
+
const visibleMessages = omitted > 0 ? messages.slice(-maxMessages) : messages;
|
|
239
|
+
|
|
240
|
+
if (omitted > 0) {
|
|
241
|
+
blocks.push({ kind: "notice", text: `… 已省略前 ${omitted} 条历史消息` });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const message of visibleMessages) {
|
|
245
|
+
blocks.push(...messageToBlocks(message));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const messageCount = session.messageCount ?? messages.length;
|
|
249
|
+
return {
|
|
250
|
+
title: buildSessionLabel(session),
|
|
251
|
+
subtitle: `${formatTimestamp(session.modified)} · 共 ${messageCount} 条消息 · ${session.cwd}`,
|
|
252
|
+
blocks: blocks.length > 0 ? blocks : [{ kind: "notice", text: "暂无可预览的消息内容。" }],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildPreviewError(session: SessionInfoLike, error: unknown): SessionPreview {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
return {
|
|
259
|
+
title: buildSessionLabel(session),
|
|
260
|
+
subtitle: `${formatTimestamp(session.modified)} · 预览暂不可用`,
|
|
261
|
+
blocks: [{ kind: "notice", text: `加载会话预览失败: ${message}` }],
|
|
262
|
+
error: message,
|
|
263
|
+
};
|
|
264
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unifan/pi-unifan-zh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pi Coding Agent 中文扩展全家桶与独立插件集",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "extensions/sessions/index.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/821869798/pi-unifan-zh.git"
|
|
10
|
+
},
|
|
11
|
+
"pi": {
|
|
12
|
+
"extensions": [
|
|
13
|
+
"./extensions/sessions/index.ts",
|
|
14
|
+
"./extensions/review/index.ts"
|
|
15
|
+
],
|
|
16
|
+
"subagents": {
|
|
17
|
+
"agents": [
|
|
18
|
+
"./extensions/review/agents"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"skills": [
|
|
22
|
+
"./skills"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pi-extension",
|
|
27
|
+
"pi-coding-agent",
|
|
28
|
+
"chinese",
|
|
29
|
+
"sessions",
|
|
30
|
+
"review",
|
|
31
|
+
"tui"
|
|
32
|
+
],
|
|
33
|
+
"author": "unifan",
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
|
36
|
+
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 🎯 Pi Skills 目录 (自定义技能库)
|
|
2
|
+
|
|
3
|
+
此目录用于存放你自定义的 Pi Agent Skills。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🛠️ 如何添加一个新 Skill?
|
|
8
|
+
|
|
9
|
+
每个 Skill 是一个单独的文件夹,包含一个核心的 `SKILL.md`:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
skills/
|
|
13
|
+
└── my-unity-helper/
|
|
14
|
+
└── SKILL.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### `SKILL.md` 格式规范:
|
|
18
|
+
```markdown
|
|
19
|
+
---
|
|
20
|
+
name: my-unity-helper
|
|
21
|
+
description: 专门用于处理 Unity C# 业务逻辑与性能优化的自定义技能
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# 指令与规则
|
|
25
|
+
1. 异步编程一律使用 UniTask。
|
|
26
|
+
2. 避免在 Update 中进行任何 GC 堆内存分配。
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
当安装本仓库后,Pi 会自动发现并加载此目录下的所有 Skill。
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist"
|
|
11
|
+
},
|
|
12
|
+
"include": ["extensions/**/*", "src/**/*"]
|
|
13
|
+
}
|
|
14
|
+
|