@ganziliang/desktop-pet 0.1.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.
@@ -0,0 +1,132 @@
1
+ {
2
+ "walk": {
3
+ "file": "pet-walk.png",
4
+ "frames": 4,
5
+ "frameW": 459,
6
+ "frameH": 637,
7
+ "refPx": 620,
8
+ "axis": "h",
9
+ "padX": 10,
10
+ "padY": 8
11
+ },
12
+ "run": {
13
+ "file": "pet-run.png",
14
+ "frames": 4,
15
+ "frameW": 558,
16
+ "frameH": 663,
17
+ "refPx": 620,
18
+ "axis": "h",
19
+ "padX": 10,
20
+ "padY": 8
21
+ },
22
+ "idle": {
23
+ "file": "pet-idle.png",
24
+ "frames": 1,
25
+ "frameW": 419,
26
+ "frameH": 624,
27
+ "refPx": 620,
28
+ "axis": "h",
29
+ "padX": 10,
30
+ "padY": 8
31
+ },
32
+ "idle-blink": {
33
+ "file": "pet-idle-blink.png",
34
+ "frames": 1,
35
+ "frameW": 419,
36
+ "frameH": 624,
37
+ "refPx": 620,
38
+ "axis": "h",
39
+ "padX": 10,
40
+ "padY": 8
41
+ },
42
+ "swim": {
43
+ "file": "pet-swim.png",
44
+ "frames": 6,
45
+ "frameW": 472,
46
+ "frameH": 323,
47
+ "refPx": 420,
48
+ "axis": "w",
49
+ "padX": 10,
50
+ "padY": 8
51
+ },
52
+ "awaken-seraph-charge": {
53
+ "file": "seraph-charge.png",
54
+ "frames": 4,
55
+ "frameW": 570,
56
+ "frameH": 662,
57
+ "refPx": 620,
58
+ "axis": "h",
59
+ "padX": 10,
60
+ "padY": 8
61
+ },
62
+ "awaken-thunder-charge": {
63
+ "file": "thunder-charge.png",
64
+ "frames": 4,
65
+ "frameW": 550,
66
+ "frameH": 665,
67
+ "refPx": 620,
68
+ "axis": "h",
69
+ "padX": 10,
70
+ "padY": 8
71
+ },
72
+ "awaken-astral-charge": {
73
+ "file": "astral-charge.png",
74
+ "frames": 4,
75
+ "frameW": 587,
76
+ "frameH": 669,
77
+ "refPx": 620,
78
+ "axis": "h",
79
+ "padX": 10,
80
+ "padY": 8
81
+ },
82
+ "awaken-nocturne-charge": {
83
+ "file": "nocturne-charge.png",
84
+ "frames": 4,
85
+ "frameW": 436,
86
+ "frameH": 662,
87
+ "refPx": 620,
88
+ "axis": "h",
89
+ "padX": 10,
90
+ "padY": 8
91
+ },
92
+ "awaken-seraph-burst": {
93
+ "file": "seraph-burst.png",
94
+ "frames": 4,
95
+ "frameW": 527,
96
+ "frameH": 670,
97
+ "refPx": 620,
98
+ "axis": "h",
99
+ "padX": 10,
100
+ "padY": 8
101
+ },
102
+ "awaken-thunder-burst": {
103
+ "file": "thunder-burst.png",
104
+ "frames": 4,
105
+ "frameW": 660,
106
+ "frameH": 771,
107
+ "refPx": 620,
108
+ "axis": "h",
109
+ "padX": 10,
110
+ "padY": 8
111
+ },
112
+ "awaken-nocturne-burst": {
113
+ "file": "nocturne-burst.png",
114
+ "frames": 4,
115
+ "frameW": 537,
116
+ "frameH": 716,
117
+ "refPx": 620,
118
+ "axis": "h",
119
+ "padX": 10,
120
+ "padY": 8
121
+ },
122
+ "awaken-astral-burst": {
123
+ "file": "astral-burst.png",
124
+ "frames": 4,
125
+ "frameW": 539,
126
+ "frameH": 683,
127
+ "refPx": 620,
128
+ "axis": "h",
129
+ "padX": 10,
130
+ "padY": 8
131
+ }
132
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * desktop-pet 启动器
6
+ *
7
+ * 用包内自带的 electron 跑包目录,命令行参数原样透传给主进程
8
+ * (--snapshot / --anim=<walk|run|swim|awaken:seraph> / --no-auto 都由 main.js 解析)。
9
+ *
10
+ * 用法:
11
+ * npx @ganziliang/desktop-pet
12
+ * desktop-pet --snapshot
13
+ */
14
+
15
+ const { spawn } = require('node:child_process');
16
+ const path = require('node:path');
17
+
18
+ let electronPath;
19
+ try {
20
+ // 不在 electron 进程里时,require('electron') 返回可执行文件的绝对路径
21
+ electronPath = require('electron');
22
+ } catch {
23
+ console.error('[desktop-pet] 找不到 electron,请先执行 npm install');
24
+ process.exit(1);
25
+ }
26
+
27
+ const appDir = path.join(__dirname, '..');
28
+
29
+ const child = spawn(electronPath, [appDir, ...process.argv.slice(2)], {
30
+ stdio: 'inherit',
31
+ windowsHide: false,
32
+ env: process.env,
33
+ });
34
+
35
+ child.on('error', (err) => {
36
+ console.error('[desktop-pet] 启动失败:', err.message);
37
+ process.exit(1);
38
+ });
39
+
40
+ child.on('close', (code, signal) => {
41
+ if (signal) process.kill(process.pid, signal);
42
+ else process.exit(code ?? 0);
43
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * pi ↔ 桌面小控件 桥接扩展
3
+ *
4
+ * 作用:
5
+ * 1. 把 agent 每条回复的摘要推给桌面角色,让它在气泡里说出来
6
+ * 2. 同时给 system prompt 追加甜妹语气指令(和 sweet-tone 扩展同源思路)
7
+ *
8
+ * 安装:
9
+ * 复制到 ~/.pi/agent/extensions/pi-pet-bridge.ts,然后 /reload
10
+ * 前置:desktop-pet 正在运行(npm start),HTTP 接口监听 127.0.0.1:8520
11
+ *
12
+ * 开关:/pet(切换推送)、/pet-tone(切换语气注入)
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+
17
+ const PET_URL = process.env.PET_URL ?? "http://127.0.0.1:8520";
18
+
19
+ /** 语气指令:去掉套话,短句、软语气,但绝不牺牲技术准确性 */
20
+ const SWEET_TONE = `
21
+
22
+ IMPORTANT(语气要求,不改变技术准确性):
23
+ - 用简体中文回复的日常说明部分,采用亲切、软萌的语气(可以少量用「呀 / 呢 / 哦 / ~」和颜文字)
24
+ - 结论、代码、命令、报错信息必须原样准确,不加任何装饰性文字,不能因为语气而含糊
25
+ - 简短优先:语气词只加在句尾,不要写长篇卖萌段落
26
+ `;
27
+
28
+ async function push(text: string, mood?: string): Promise<boolean> {
29
+ try {
30
+ const res = await fetch(`${PET_URL}/say`, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ text, mood }),
34
+ });
35
+ return res.ok;
36
+ } catch {
37
+ return false; // 桌宠没开时静默失败,绝不干扰主流程
38
+ }
39
+ }
40
+
41
+ /** 从 markdown 回复里抽一句适合气泡显示的话 */
42
+ function digest(content: string): string {
43
+ const plain = content
44
+ .replace(/```[\s\S]*?```/g, " ")
45
+ .replace(/`([^`]*)`/g, "$1")
46
+ .replace(/[*#>_~]/g, "")
47
+ .replace(/\s+/g, " ")
48
+ .trim();
49
+
50
+ if (!plain) return "搞定啦!";
51
+ const firstSentence = plain.split(/(?<=[。!?!?])/)[0] ?? plain;
52
+ return firstSentence.slice(0, 80);
53
+ }
54
+
55
+ export default function petBridge(pi: ExtensionAPI) {
56
+ let pushEnabled = true;
57
+ let toneEnabled = true;
58
+
59
+ pi.registerCommand("pet", {
60
+ description: "切换「把回复推给桌面小控件」",
61
+ handler: async (_args, ctx) => {
62
+ pushEnabled = !pushEnabled;
63
+ ctx.ui.notify(pushEnabled ? "桌宠推送已开启" : "桌宠推送已关闭", "info");
64
+ if (pushEnabled) await push("我回来啦~");
65
+ },
66
+ });
67
+
68
+ pi.registerCommand("pet-tone", {
69
+ description: "切换甜妹语气注入",
70
+ handler: async (_args, ctx) => {
71
+ toneEnabled = !toneEnabled;
72
+ ctx.ui.notify(toneEnabled ? "甜妹语气已开启" : "甜妹语气已关闭", "info");
73
+ },
74
+ });
75
+
76
+ pi.on("before_agent_start", async (event) => {
77
+ if (!toneEnabled) return;
78
+ return { systemPrompt: event.systemPrompt + SWEET_TONE };
79
+ });
80
+
81
+ // 每轮 agent 结束把最后一句话推给桌宠
82
+ pi.on("agent_end", async (event) => {
83
+ if (!pushEnabled) return;
84
+ const messages = event.messages ?? [];
85
+ const last = [...messages].reverse().find((m) => m.role === "assistant");
86
+ if (!last) return;
87
+
88
+ const text =
89
+ typeof last.content === "string"
90
+ ? last.content
91
+ : Array.isArray(last.content)
92
+ ? last.content
93
+ .filter((part) => part.type === "text")
94
+ .map((part) => part.text)
95
+ .join(" ")
96
+ : "";
97
+
98
+ if (!text.trim()) return;
99
+ await push(digest(text));
100
+ });
101
+ }