pi-web-ui 0.63.1 → 0.63.3
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/LICENSE +21 -21
- package/README.md +504 -504
- package/README.zh-CN.md +427 -427
- package/bin/pi-web-ui.mjs +1809 -1809
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/nginx-subpath.conf +88 -88
- package/deploy/pi-web-ui-task.xml +54 -54
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +228 -11
- package/dist/server/attachments.js +3 -3
- package/dist/server/client-state.js +18 -0
- package/dist/server/dsh/dsh-agent-service.js +21 -0
- package/dist/server/dsh/runtime/cordis.yml +1 -1
- package/dist/server/dsh/runtime/goal-rpc.mjs +645 -645
- package/dist/server/dsh/runtime/launcher.mjs +164 -164
- package/dist/server/dsh/runtime/override.patch.yml +71 -71
- package/dist/server/dsh/runtime/runtime-root.mjs +86 -86
- package/dist/server/index.js +11 -0
- package/dist/server/marker-service.js +270 -0
- package/dist/server/markers/builtins/notify.js +20 -0
- package/dist/server/markers/builtins/rename.js +102 -0
- package/dist/server/markers/builtins/services.js +119 -0
- package/dist/server/markers/builtins/todo.js +130 -0
- package/dist/server/markers/index.js +24 -0
- package/dist/server/markers/marker.js +53 -0
- package/dist/server/markers/registry.js +28 -0
- package/dist/server/markers/store.js +47 -0
- package/dist/server/settings-service.js +3 -0
- package/dist/server/slash-commands.js +34 -0
- package/dist/server/terminals.js +15 -4
- package/dist/server/themes.js +24 -5
- package/dist/server/vision-bridge.js +9 -9
- package/extensions/webui.ts +190 -190
- package/package.json +1 -1
- package/themes/cyberpunk.css +81 -80
- package/themes/dazzle.css +81 -80
- package/themes/md-preview.css +98 -97
- package/themes/white.css +160 -159
- package/web/dist/assets/TerminalPanel-BFrV6B8W.js +2 -0
- package/web/dist/assets/index-BFoSybNe.js +324 -0
- package/web/dist/assets/index-D9G_7fPE.css +10 -0
- package/web/dist/favicon.svg +8 -8
- package/web/dist/icons/icon-1024.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/maskable-1024.png +0 -0
- package/web/dist/icons/maskable-192.png +0 -0
- package/web/dist/icons/maskable-512.png +0 -0
- package/web/dist/index.html +23 -17
- package/web/dist/manifest.webmanifest +50 -0
- package/web/dist/sw.js +126 -0
- package/web/public/favicon.svg +8 -8
- package/web/public/icons/icon-1024.png +0 -0
- package/web/public/icons/icon-192.png +0 -0
- package/web/public/icons/icon-512.png +0 -0
- package/web/public/icons/maskable-1024.png +0 -0
- package/web/public/icons/maskable-192.png +0 -0
- package/web/public/icons/maskable-512.png +0 -0
- package/web/public/manifest.webmanifest +50 -0
- package/web/public/sw.js +126 -0
- package/web/dist/assets/TerminalPanel-DwutHqgi.js +0 -2
- package/web/dist/assets/index-CJoS1bmZ.js +0 -323
- package/web/dist/assets/index-CrDJOsa5.css +0 -10
package/dist/server/index.js
CHANGED
|
@@ -604,6 +604,12 @@ wss.on("connection", (ws) => {
|
|
|
604
604
|
case "delete_session":
|
|
605
605
|
void cs.deleteSession(msg.path);
|
|
606
606
|
break;
|
|
607
|
+
case "rename_session":
|
|
608
|
+
void cs.renameSession(msg.path, msg.name);
|
|
609
|
+
break;
|
|
610
|
+
case "rename_conversation":
|
|
611
|
+
void cs.renameConversation(msg.id, msg.name);
|
|
612
|
+
break;
|
|
607
613
|
case "dismiss_conversation":
|
|
608
614
|
void cs.dismissConversation(msg.id);
|
|
609
615
|
break;
|
|
@@ -724,6 +730,9 @@ wss.on("connection", (ws) => {
|
|
|
724
730
|
case "terminal_kill":
|
|
725
731
|
cs.getTerminalManager(msg.conversationId)?.kill(msg.terminalId);
|
|
726
732
|
break;
|
|
733
|
+
case "rename_terminal":
|
|
734
|
+
cs.getTerminalManager(msg.conversationId)?.rename(msg.terminalId, msg.title);
|
|
735
|
+
break;
|
|
727
736
|
case "run_command":
|
|
728
737
|
cs.getTerminalManager(msg.conversationId)?.runCommand(msg.terminalId, msg.command, msg.cols, msg.rows, cs.getTerminalCwd(msg.conversationId));
|
|
729
738
|
break;
|
|
@@ -778,6 +787,8 @@ wss.on("connection", (ws) => {
|
|
|
778
787
|
visionBridgePrompt: msg.visionBridgePrompt,
|
|
779
788
|
reviewPrompt: msg.reviewPrompt,
|
|
780
789
|
reviewDisabledSkills: msg.reviewDisabledSkills,
|
|
790
|
+
markersEnabled: msg.markersEnabled,
|
|
791
|
+
disabledMarkers: msg.disabledMarkers,
|
|
781
792
|
});
|
|
782
793
|
break;
|
|
783
794
|
case "extensions_reload":
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marker-service.ts — 标记服务(内置版 pi-marker-tools)。
|
|
3
|
+
*/
|
|
4
|
+
import { ensureMarkersRegistered, parseMarkers, getMarker, allMarkers, collectGuidance, listMarkerNames, } from "./markers/index.js";
|
|
5
|
+
import { loadStateFromBranch, appendSnapshot } from "./markers/store.js";
|
|
6
|
+
import { TODO_NAMESPACE, initTodoState, describeTodos } from "./markers/builtins/todo.js";
|
|
7
|
+
import { SVC_NAMESPACE, initServiceState, describeServices } from "./markers/builtins/services.js";
|
|
8
|
+
ensureMarkersRegistered();
|
|
9
|
+
const DEFAULT_MARKERS = {
|
|
10
|
+
markersEnabled: true,
|
|
11
|
+
disabledMarkers: [],
|
|
12
|
+
};
|
|
13
|
+
export class MarkerService {
|
|
14
|
+
host;
|
|
15
|
+
settings;
|
|
16
|
+
memStore = new Map();
|
|
17
|
+
overlayCache = new Map();
|
|
18
|
+
constructor(host) {
|
|
19
|
+
this.host = host;
|
|
20
|
+
this.settings = this.host.stateStore.getMarkerSettings(this.host.clientId);
|
|
21
|
+
}
|
|
22
|
+
get current() {
|
|
23
|
+
return { ...this.settings, disabledMarkers: [...this.settings.disabledMarkers] };
|
|
24
|
+
}
|
|
25
|
+
get allMarkerNames() {
|
|
26
|
+
return listMarkerNames();
|
|
27
|
+
}
|
|
28
|
+
isGlobalEnabled() {
|
|
29
|
+
return this.settings.markersEnabled;
|
|
30
|
+
}
|
|
31
|
+
isMarkerEnabled(name) {
|
|
32
|
+
if (!this.settings.markersEnabled)
|
|
33
|
+
return false;
|
|
34
|
+
if ((name === "rename" || name === "title") && this.settings.disabledMarkers.includes("conv"))
|
|
35
|
+
return false;
|
|
36
|
+
if (name === "conv" && (this.settings.disabledMarkers.includes("rename") || this.settings.disabledMarkers.includes("title")))
|
|
37
|
+
return false;
|
|
38
|
+
return !this.settings.disabledMarkers.includes(name);
|
|
39
|
+
}
|
|
40
|
+
buildGuidance() {
|
|
41
|
+
if (!this.settings.markersEnabled)
|
|
42
|
+
return "";
|
|
43
|
+
const disabled = new Set(this.settings.disabledMarkers);
|
|
44
|
+
const lines = collectGuidance(disabled);
|
|
45
|
+
if (lines.length === 0)
|
|
46
|
+
return "";
|
|
47
|
+
return ("\n\n────────── 内联标记工具(重要)──────────\n" +
|
|
48
|
+
"状态类操作(任务、标记位、通知)请直接写在回答正文里,采用内联标记语法,而不要调用对应工具。\n" +
|
|
49
|
+
"插件会替你执行并把标记从展示文本中移除,不会中断你的回答。\n\n" +
|
|
50
|
+
lines.join("\n") +
|
|
51
|
+
"\n──────────\n");
|
|
52
|
+
}
|
|
53
|
+
setEnabled(enabled) {
|
|
54
|
+
this.settings.markersEnabled = !!enabled;
|
|
55
|
+
this.host.stateStore.saveMarkerSettings(this.host.clientId, this.settings);
|
|
56
|
+
}
|
|
57
|
+
toggleMarker(name, enabled) {
|
|
58
|
+
const set = new Set(this.settings.disabledMarkers);
|
|
59
|
+
const group = name === "conv" || name === "rename" || name === "title" ? ["conv", "rename", "title"] : [name];
|
|
60
|
+
for (const n of group) {
|
|
61
|
+
if (enabled)
|
|
62
|
+
set.delete(n);
|
|
63
|
+
else
|
|
64
|
+
set.add(n);
|
|
65
|
+
}
|
|
66
|
+
this.settings.disabledMarkers = [...set];
|
|
67
|
+
this.host.stateStore.saveMarkerSettings(this.host.clientId, this.settings);
|
|
68
|
+
}
|
|
69
|
+
setAll(settings) {
|
|
70
|
+
if (settings.markersEnabled !== undefined)
|
|
71
|
+
this.settings.markersEnabled = !!settings.markersEnabled;
|
|
72
|
+
if (settings.disabledMarkers !== undefined) {
|
|
73
|
+
// 归一化 rename 别名:若 conv 被禁用则同步禁用别名
|
|
74
|
+
const s = new Set(settings.disabledMarkers);
|
|
75
|
+
if (s.has("conv") || s.has("rename") || s.has("title")) {
|
|
76
|
+
s.add("conv");
|
|
77
|
+
s.add("rename");
|
|
78
|
+
s.add("title");
|
|
79
|
+
}
|
|
80
|
+
this.settings.disabledMarkers = [...s];
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.host.stateStore.saveMarkerSettings(this.host.clientId, this.settings);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.host.stateStore.saveMarkerSettings(this.host.clientId, this.settings);
|
|
87
|
+
}
|
|
88
|
+
// -- state helpers --
|
|
89
|
+
memFor(convId, ns) {
|
|
90
|
+
return this.memStore.get(convId)?.get(ns);
|
|
91
|
+
}
|
|
92
|
+
setMem(convId, ns, state) {
|
|
93
|
+
let m = this.memStore.get(convId);
|
|
94
|
+
if (!m) {
|
|
95
|
+
m = new Map();
|
|
96
|
+
this.memStore.set(convId, m);
|
|
97
|
+
}
|
|
98
|
+
m.set(ns, state);
|
|
99
|
+
}
|
|
100
|
+
getState(convId, namespace, init) {
|
|
101
|
+
const mgr = this.host.getSessionManager(convId);
|
|
102
|
+
if (mgr?.getBranch) {
|
|
103
|
+
try {
|
|
104
|
+
const branch = mgr.getBranch();
|
|
105
|
+
const fromBranch = loadStateFromBranch(branch, namespace);
|
|
106
|
+
if (fromBranch !== undefined) {
|
|
107
|
+
this.setMem(convId, namespace, fromBranch);
|
|
108
|
+
return structuredClone(fromBranch);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
const mem = this.memFor(convId, namespace);
|
|
114
|
+
if (mem !== undefined)
|
|
115
|
+
return structuredClone(mem);
|
|
116
|
+
return init();
|
|
117
|
+
}
|
|
118
|
+
saveState(convId, namespace, state) {
|
|
119
|
+
this.setMem(convId, namespace, structuredClone(state));
|
|
120
|
+
const mgr = this.host.getSessionManager(convId);
|
|
121
|
+
appendSnapshot(mgr, namespace, state);
|
|
122
|
+
}
|
|
123
|
+
// -- parse & execute --
|
|
124
|
+
async handleAssistantText(conversationId, text) {
|
|
125
|
+
if (!text || !this.settings.markersEnabled)
|
|
126
|
+
return;
|
|
127
|
+
const tokens = parseMarkers(text);
|
|
128
|
+
if (tokens.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
const disabled = new Set(this.settings.disabledMarkers);
|
|
131
|
+
const states = new Map();
|
|
132
|
+
const getOrInit = (ns) => {
|
|
133
|
+
let st = states.get(ns);
|
|
134
|
+
if (st !== undefined)
|
|
135
|
+
return st;
|
|
136
|
+
if (ns === TODO_NAMESPACE)
|
|
137
|
+
st = this.getState(conversationId, ns, initTodoState);
|
|
138
|
+
else if (ns === SVC_NAMESPACE)
|
|
139
|
+
st = this.getState(conversationId, ns, initServiceState);
|
|
140
|
+
else {
|
|
141
|
+
const marker = getMarker(ns);
|
|
142
|
+
st = marker?.init ? marker.init() : {};
|
|
143
|
+
}
|
|
144
|
+
states.set(ns, st);
|
|
145
|
+
return st;
|
|
146
|
+
};
|
|
147
|
+
const dirty = new Set();
|
|
148
|
+
for (const token of tokens) {
|
|
149
|
+
if (disabled.has(token.tool))
|
|
150
|
+
continue;
|
|
151
|
+
const marker = getMarker(token.tool);
|
|
152
|
+
if (!marker)
|
|
153
|
+
continue;
|
|
154
|
+
const state = getOrInit(token.tool);
|
|
155
|
+
const ctx = {
|
|
156
|
+
conversationId,
|
|
157
|
+
notify: (msg, level) => {
|
|
158
|
+
this.host.emit({ type: "notice", level: level ?? "info", text: msg });
|
|
159
|
+
},
|
|
160
|
+
renameConversation: (title) => {
|
|
161
|
+
this.host.renameConversation(conversationId, title);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = await marker.apply(token, ctx, state);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
result = { applied: false, error: `执行异常: ${e?.message ?? String(e)}` };
|
|
170
|
+
}
|
|
171
|
+
if (result.applied) {
|
|
172
|
+
if (token.tool !== "notify" && token.tool !== "conv" && token.tool !== "rename" && token.tool !== "title") {
|
|
173
|
+
dirty.add(token.tool);
|
|
174
|
+
}
|
|
175
|
+
else if (token.tool === "conv" || token.tool === "rename" || token.tool === "title") {
|
|
176
|
+
// rename 不落库,已直接重命名
|
|
177
|
+
}
|
|
178
|
+
else if (token.tool === "notify") {
|
|
179
|
+
// 通知不落库
|
|
180
|
+
}
|
|
181
|
+
if (token.tool === "todo" || token.tool === "svc")
|
|
182
|
+
dirty.add(token.tool);
|
|
183
|
+
}
|
|
184
|
+
else if (result.error) {
|
|
185
|
+
this.host.emit({ type: "notice", level: "warning", text: `[${token.tool}] ${result.error}` });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const ns of dirty) {
|
|
189
|
+
const mutated = states.get(ns);
|
|
190
|
+
if (mutated !== undefined)
|
|
191
|
+
this.saveState(conversationId, ns, mutated);
|
|
192
|
+
}
|
|
193
|
+
this.pushOverlay(conversationId);
|
|
194
|
+
}
|
|
195
|
+
pushOverlay(conversationId) {
|
|
196
|
+
const lines = [];
|
|
197
|
+
for (const m of allMarkers()) {
|
|
198
|
+
if (this.settings.disabledMarkers.includes(m.name))
|
|
199
|
+
continue;
|
|
200
|
+
if (!m.overlay)
|
|
201
|
+
continue;
|
|
202
|
+
let state;
|
|
203
|
+
if (m.name === TODO_NAMESPACE)
|
|
204
|
+
state = this.getState(conversationId, m.name, initTodoState);
|
|
205
|
+
else if (m.name === SVC_NAMESPACE)
|
|
206
|
+
state = this.getState(conversationId, m.name, initServiceState);
|
|
207
|
+
else {
|
|
208
|
+
state = this.getState(conversationId, m.name, () => m.init?.() ?? {});
|
|
209
|
+
if (state === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const ctx = {
|
|
213
|
+
conversationId,
|
|
214
|
+
notify: () => { },
|
|
215
|
+
renameConversation: (t) => this.host.renameConversation(conversationId, t),
|
|
216
|
+
};
|
|
217
|
+
const ov = m.overlay(state, ctx);
|
|
218
|
+
if (!ov)
|
|
219
|
+
continue;
|
|
220
|
+
lines.push(`[${ov.tool}]`);
|
|
221
|
+
lines.push(...ov.lines.map((l) => ` ${l}`));
|
|
222
|
+
}
|
|
223
|
+
const key = `markers:${conversationId}`;
|
|
224
|
+
const prev = this.overlayCache.get(key);
|
|
225
|
+
const next = lines.length ? lines : [];
|
|
226
|
+
if (prev && prev.join("\n") === next.join("\n"))
|
|
227
|
+
return;
|
|
228
|
+
this.overlayCache.set(key, next);
|
|
229
|
+
if (lines.length > 0) {
|
|
230
|
+
this.host.emit({ type: "widgets", widgets: [{ key: "markers", lines }] });
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
this.host.emit({ type: "widgets", widgets: [] });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
describe(conversationId, tool, includeDeleted = false) {
|
|
237
|
+
if (tool === "svc") {
|
|
238
|
+
const st = this.getState(conversationId, SVC_NAMESPACE, initServiceState);
|
|
239
|
+
return describeServices(st);
|
|
240
|
+
}
|
|
241
|
+
const st = this.getState(conversationId, TODO_NAMESPACE, initTodoState);
|
|
242
|
+
const visible = st.tasks.filter((t) => includeDeleted || t.status !== "deleted");
|
|
243
|
+
if (visible.length === 0)
|
|
244
|
+
return "No todos";
|
|
245
|
+
return visible.map((t) => `[${t.status}] #${t.id}: ${t.subject}`).join("\n");
|
|
246
|
+
}
|
|
247
|
+
getRawState(conversationId, namespace) {
|
|
248
|
+
if (namespace === TODO_NAMESPACE)
|
|
249
|
+
return this.getState(conversationId, namespace, initTodoState);
|
|
250
|
+
if (namespace === SVC_NAMESPACE)
|
|
251
|
+
return this.getState(conversationId, namespace, initServiceState);
|
|
252
|
+
const m = getMarker(namespace);
|
|
253
|
+
return this.getState(conversationId, namespace, () => m?.init?.() ?? {});
|
|
254
|
+
}
|
|
255
|
+
/** 供设置面板展示的 marker 目录(含启用状态)。 */
|
|
256
|
+
/** UI 过滤:rename/title 是 conv 的别名,不单独展示。 */
|
|
257
|
+
listForUi() {
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const m of allMarkers()) {
|
|
261
|
+
if (m.name === "rename" || m.name === "title")
|
|
262
|
+
continue;
|
|
263
|
+
if (seen.has(m.name))
|
|
264
|
+
continue;
|
|
265
|
+
seen.add(m.name);
|
|
266
|
+
out.push({ name: m.name, enabled: this.isMarkerEnabled(m.name), guidance: m.guidance });
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtins/notify.ts — 纯提醒标记。
|
|
3
|
+
*/
|
|
4
|
+
export const notifyMarker = {
|
|
5
|
+
name: "notify",
|
|
6
|
+
guidance: [
|
|
7
|
+
"- [[notify:<级别>:<内容>]] 仅向用户显示一个非打断性提醒,不会进入正文。级别为 info|warning|success|error。",
|
|
8
|
+
],
|
|
9
|
+
async apply(token, ctx) {
|
|
10
|
+
const level = token.op || token.kwargs["level"] || "info";
|
|
11
|
+
const text = token.kwargs["text"] || token.args.join(" ") || "";
|
|
12
|
+
if (!text)
|
|
13
|
+
return { applied: false, error: "notify 需要内容" };
|
|
14
|
+
const safe = (level === "warning" || level === "error" ? level : "info");
|
|
15
|
+
ctx.notify(text, safe);
|
|
16
|
+
return { applied: true, feedback: "notified" };
|
|
17
|
+
},
|
|
18
|
+
overlay: undefined,
|
|
19
|
+
init: () => undefined,
|
|
20
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtins/rename.ts — 重命名当前对话标记。
|
|
3
|
+
*
|
|
4
|
+
* 需求:加个重命名当前对话 marker。
|
|
5
|
+
*
|
|
6
|
+
* 语法:
|
|
7
|
+
* [[conv:rename:<新标题>]] 重命名当前对话
|
|
8
|
+
* [[rename:set:<新标题>]] 同上(兼容别名)
|
|
9
|
+
* [[title:rename:<新标题>]] 同上
|
|
10
|
+
* [[rename:new:<新标题>]] 同上
|
|
11
|
+
*
|
|
12
|
+
* 兼容:rename 前缀的任意 op 都会被视为重命名,例如 [[rename:set:My Chat]]、
|
|
13
|
+
* [[rename:to:My Chat]]。标题来自 args[0] + kwargs[text/name/title] 的
|
|
14
|
+
* 拼接,未提供则报错。
|
|
15
|
+
*
|
|
16
|
+
* 持久化:通过宿主回调直接改对话标题(内存 + 磁盘 transcript session_info),
|
|
17
|
+
* 不需要额外状态。
|
|
18
|
+
*/
|
|
19
|
+
export const RENAME_NAMESPACE = "conv";
|
|
20
|
+
function extractTitle(token) {
|
|
21
|
+
// args[0] 是主标题;kwargs 兼容 text/name/title
|
|
22
|
+
const fromArgs = token.args.join(" ").trim();
|
|
23
|
+
const fromKw = (token.kwargs["text"] ?? token.kwargs["name"] ?? token.kwargs["title"] ?? "").trim();
|
|
24
|
+
if (fromArgs && fromKw)
|
|
25
|
+
return `${fromArgs} ${fromKw}`.trim();
|
|
26
|
+
return fromArgs || fromKw;
|
|
27
|
+
}
|
|
28
|
+
export const renameMarker = {
|
|
29
|
+
name: "conv",
|
|
30
|
+
guidance: [
|
|
31
|
+
"- 重命名当前对话:[[conv:rename:<新标题>]](或 [[rename:set:<标题>]])。标题尽量简短、能概括本次任务。",
|
|
32
|
+
],
|
|
33
|
+
async apply(token, ctx) {
|
|
34
|
+
if (token.op !== "rename") {
|
|
35
|
+
return { applied: false, error: `conv 未知操作: ${token.op}(当前仅支持 conv:rename)` };
|
|
36
|
+
}
|
|
37
|
+
const title = extractTitle(token);
|
|
38
|
+
if (!title)
|
|
39
|
+
return { applied: false, error: "conv:rename 需要一个标题参数 [[conv:rename:<新标题>]]" };
|
|
40
|
+
if (title.length > 80)
|
|
41
|
+
return { applied: false, error: "标题过长(最多 80 字)" };
|
|
42
|
+
if (!ctx.renameConversation)
|
|
43
|
+
return { applied: false, error: "当前环境不支持重命名" };
|
|
44
|
+
try {
|
|
45
|
+
ctx.renameConversation(title);
|
|
46
|
+
ctx.notify(`已重命名为:${title}`, "info");
|
|
47
|
+
return { applied: true, feedback: `renamed to "${title}"` };
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return { applied: false, error: `重命名失败: ${e.message ?? String(e)}` };
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
overlay: undefined,
|
|
54
|
+
init: () => undefined,
|
|
55
|
+
};
|
|
56
|
+
/** 别名:[[rename:set:标题]] 等同 [[conv:rename:标题]],方便模型直觉书写。 */
|
|
57
|
+
export const renameAliasMarker = {
|
|
58
|
+
name: "rename",
|
|
59
|
+
guidance: [
|
|
60
|
+
"- [[rename:set:<新标题>]] 同 [[conv:rename:<新标题>]]:重命名当前对话。",
|
|
61
|
+
],
|
|
62
|
+
async apply(token, ctx) {
|
|
63
|
+
// 兼容任意 op:只要能取到标题就重命名
|
|
64
|
+
const title = extractTitle(token) || token.op?.trim() || "";
|
|
65
|
+
// 若 token 是 [[rename:My Title:]] 形式,op=My Title, args 空 —— 用 op 当标题
|
|
66
|
+
const effective = title || token.op;
|
|
67
|
+
if (!effective?.trim())
|
|
68
|
+
return { applied: false, error: "rename 需要标题参数 [[rename:set:<新标题>]]" };
|
|
69
|
+
const trimmed = effective.trim().slice(0, 80);
|
|
70
|
+
if (!ctx.renameConversation)
|
|
71
|
+
return { applied: false, error: "当前环境不支持重命名" };
|
|
72
|
+
try {
|
|
73
|
+
ctx.renameConversation(trimmed);
|
|
74
|
+
ctx.notify(`已重命名为:${trimmed}`, "info");
|
|
75
|
+
return { applied: true, feedback: `renamed to "${trimmed}"` };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return { applied: false, error: `重命名失败: ${e.message ?? String(e)}` };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
overlay: undefined,
|
|
82
|
+
init: () => undefined,
|
|
83
|
+
};
|
|
84
|
+
/** title 前缀别名:[[title:rename:标题]] */
|
|
85
|
+
export const titleAliasMarker = {
|
|
86
|
+
name: "title",
|
|
87
|
+
guidance: [],
|
|
88
|
+
async apply(token, ctx) {
|
|
89
|
+
if (token.op !== "rename")
|
|
90
|
+
return { applied: false, error: `title 未知操作: ${token.op}` };
|
|
91
|
+
const title = extractTitle(token);
|
|
92
|
+
if (!title)
|
|
93
|
+
return { applied: false, error: "title:rename 需要标题" };
|
|
94
|
+
if (!ctx.renameConversation)
|
|
95
|
+
return { applied: false, error: "当前环境不支持重命名" };
|
|
96
|
+
ctx.renameConversation(title.slice(0, 80));
|
|
97
|
+
ctx.notify(`已重命名为:${title.slice(0, 80)}`, "info");
|
|
98
|
+
return { applied: true, feedback: `renamed` };
|
|
99
|
+
},
|
|
100
|
+
overlay: undefined,
|
|
101
|
+
init: () => undefined,
|
|
102
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtins/services.ts — 后台服务登记(复刻 pi-marker-tools)。
|
|
3
|
+
*/
|
|
4
|
+
export const SVC_NAMESPACE = "svc";
|
|
5
|
+
export function initServiceState() {
|
|
6
|
+
return { services: [], nextId: 1 };
|
|
7
|
+
}
|
|
8
|
+
function parseId(raw) {
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
const n = Number(raw);
|
|
12
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
13
|
+
}
|
|
14
|
+
function toInt(raw) {
|
|
15
|
+
if (raw === undefined || raw === "")
|
|
16
|
+
return undefined;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
19
|
+
}
|
|
20
|
+
function findService(state, id) {
|
|
21
|
+
return state.services.find((s) => s.id === id);
|
|
22
|
+
}
|
|
23
|
+
function lineOf(s) {
|
|
24
|
+
const meta = [s.pid !== undefined ? `pid ${s.pid}` : "", s.port !== undefined ? `:${s.port}` : ""].filter(Boolean).join(" ");
|
|
25
|
+
return `${s.stopped ? "[x]" : "[ ]"} #${s.id}: ${s.name}${meta ? ` (${meta})` : ""}${s.command ? ` — ${s.command}` : ""}`;
|
|
26
|
+
}
|
|
27
|
+
export function describeServices(state) {
|
|
28
|
+
if (!state || state.services.length === 0)
|
|
29
|
+
return "[svc] (空)";
|
|
30
|
+
return state.services.map(lineOf).join("\n");
|
|
31
|
+
}
|
|
32
|
+
export const servicesMarker = {
|
|
33
|
+
name: "svc",
|
|
34
|
+
guidance: [
|
|
35
|
+
"- 后台服务登记(todo 风格):[[svc:add:名称,pid=,port=,cmd=]] 登记;[[svc:stop:<id>]] 标记停止;[[svc:resume:<id>]] 标记运行;[[svc:remove:<id>]] 删除;[[svc:clear:]] 清空。",
|
|
36
|
+
"- 用 bash 启动后台服务(nohup ... &、pm2 start、docker run -d 等)后,立即写 [[svc:add:...]] 登记,尽量带 pid= 和 port=;服务退出或被关闭后写 [[svc:stop:<id>]] 保持清单准确。",
|
|
37
|
+
],
|
|
38
|
+
async apply(token, _ctx, _state) {
|
|
39
|
+
const state = _state;
|
|
40
|
+
const op = token.op;
|
|
41
|
+
switch (op) {
|
|
42
|
+
case "add": {
|
|
43
|
+
const name = token.args[0]?.trim();
|
|
44
|
+
if (!name)
|
|
45
|
+
return { applied: false, error: "svc:add 需要一个名称参数 [[svc:add:<名称>,pid=,port=,cmd=]]" };
|
|
46
|
+
const svc = {
|
|
47
|
+
id: state.nextId++,
|
|
48
|
+
name,
|
|
49
|
+
command: token.kwargs["cmd"],
|
|
50
|
+
pid: toInt(token.kwargs["pid"]),
|
|
51
|
+
port: toInt(token.kwargs["port"]),
|
|
52
|
+
startedAt: Date.now(),
|
|
53
|
+
stopped: false,
|
|
54
|
+
};
|
|
55
|
+
state.services.push(svc);
|
|
56
|
+
const meta = [svc.pid !== undefined ? `pid ${svc.pid}` : "", svc.port !== undefined ? `:${svc.port}` : ""].filter(Boolean).join(" ");
|
|
57
|
+
return { applied: true, feedback: `Registered #${svc.id}: ${name}${meta ? ` (${meta})` : ""}` };
|
|
58
|
+
}
|
|
59
|
+
case "stop": {
|
|
60
|
+
const id = parseId(token.args[0]);
|
|
61
|
+
if (id === null)
|
|
62
|
+
return { applied: false, error: `svc:stop 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
63
|
+
const svc = findService(state, id);
|
|
64
|
+
if (!svc)
|
|
65
|
+
return { applied: false, error: `svc:stop 服务 #${id} 不存在` };
|
|
66
|
+
svc.stopped = true;
|
|
67
|
+
svc.stoppedAt = Date.now();
|
|
68
|
+
return { applied: true, feedback: `#${id} ${svc.name} marked stopped` };
|
|
69
|
+
}
|
|
70
|
+
case "resume": {
|
|
71
|
+
const id = parseId(token.args[0]);
|
|
72
|
+
if (id === null)
|
|
73
|
+
return { applied: false, error: `svc:resume 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
74
|
+
const svc = findService(state, id);
|
|
75
|
+
if (!svc)
|
|
76
|
+
return { applied: false, error: `svc:resume 服务 #${id} 不存在` };
|
|
77
|
+
svc.stopped = false;
|
|
78
|
+
svc.stoppedAt = undefined;
|
|
79
|
+
return { applied: true, feedback: `#${id} ${svc.name} marked running` };
|
|
80
|
+
}
|
|
81
|
+
case "remove": {
|
|
82
|
+
const id = parseId(token.args[0]);
|
|
83
|
+
if (id === null)
|
|
84
|
+
return { applied: false, error: `svc:remove 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
85
|
+
const before = state.services.length;
|
|
86
|
+
state.services = state.services.filter((s) => s.id !== id);
|
|
87
|
+
if (state.services.length === before)
|
|
88
|
+
return { applied: false, error: `svc:remove 服务 #${id} 不存在` };
|
|
89
|
+
return { applied: true, feedback: `Removed #${id}` };
|
|
90
|
+
}
|
|
91
|
+
case "clear": {
|
|
92
|
+
const n = state.services.length;
|
|
93
|
+
state.services = [];
|
|
94
|
+
state.nextId = 1;
|
|
95
|
+
return { applied: true, feedback: `Cleared ${n} service(s)` };
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return { applied: false, error: `svc 未知操作: ${op}` };
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
overlay(state) {
|
|
102
|
+
if (!state || state.services.length === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
const running = state.services.filter((s) => !s.stopped).length;
|
|
105
|
+
const lines = state.services.map((s) => {
|
|
106
|
+
const mark = s.stopped ? "○" : "●";
|
|
107
|
+
let line = ` ${mark} #${s.id} ${s.name}`;
|
|
108
|
+
if (s.port !== undefined)
|
|
109
|
+
line += ` :${s.port}`;
|
|
110
|
+
if (s.pid !== undefined)
|
|
111
|
+
line += ` (pid ${s.pid})`;
|
|
112
|
+
if (s.stopped)
|
|
113
|
+
line += " 已停止";
|
|
114
|
+
return line;
|
|
115
|
+
});
|
|
116
|
+
return { tool: "svc", lines: [`${running}/${state.services.length} running`, ...lines] };
|
|
117
|
+
},
|
|
118
|
+
init: initServiceState,
|
|
119
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtins/todo.ts — 内置任务标记(复刻 pi-marker-tools)。
|
|
3
|
+
*/
|
|
4
|
+
export const TODO_NAMESPACE = "todo";
|
|
5
|
+
export function initTodoState() {
|
|
6
|
+
return { tasks: [], nextId: 1 };
|
|
7
|
+
}
|
|
8
|
+
function findTask(state, id) {
|
|
9
|
+
return state.tasks.find((t) => t.id === id && t.status !== "deleted");
|
|
10
|
+
}
|
|
11
|
+
function parseId(raw) {
|
|
12
|
+
if (raw === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
const n = Number(raw);
|
|
15
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
16
|
+
}
|
|
17
|
+
function formatStatus(s) {
|
|
18
|
+
switch (s) {
|
|
19
|
+
case "pending": return "pending";
|
|
20
|
+
case "in_progress": return "in_progress";
|
|
21
|
+
case "completed": return "completed";
|
|
22
|
+
default: return s;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function describeTodos(state, includeDeleted = false) {
|
|
26
|
+
const visible = state.tasks.filter((t) => includeDeleted || t.status !== "deleted");
|
|
27
|
+
if (visible.length === 0)
|
|
28
|
+
return "[todo] (空)";
|
|
29
|
+
return visible
|
|
30
|
+
.map((t) => {
|
|
31
|
+
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
32
|
+
const deps = t.blockedBy.length ? ` ⛓ ${t.blockedBy.join(",")}` : "";
|
|
33
|
+
return `[${formatStatus(t.status)}] #${t.id} ${t.subject}${form}${deps}`;
|
|
34
|
+
})
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
|
37
|
+
export const todoMarker = {
|
|
38
|
+
name: "todo",
|
|
39
|
+
guidance: [
|
|
40
|
+
"# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
|
|
41
|
+
"- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
|
|
42
|
+
"- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
|
|
43
|
+
"- 想查看/list 当前任务列表时,才用 `markers_list` 工具(读操作走工具)。",
|
|
44
|
+
"- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
|
|
45
|
+
],
|
|
46
|
+
async apply(token, _ctx, _state) {
|
|
47
|
+
const state = _state;
|
|
48
|
+
const op = token.op;
|
|
49
|
+
switch (op) {
|
|
50
|
+
case "new": {
|
|
51
|
+
const subject = token.args[0]?.trim();
|
|
52
|
+
if (!subject)
|
|
53
|
+
return { applied: false, error: "todo:new 需要一个主题参数 [[todo:new:<主题>]]" };
|
|
54
|
+
const id = state.nextId++;
|
|
55
|
+
state.tasks.push({ id, subject, status: "pending", blockedBy: [], createdAt: Date.now() });
|
|
56
|
+
return { applied: true, feedback: `Created #${id}: ${subject} (pending)` };
|
|
57
|
+
}
|
|
58
|
+
case "set": {
|
|
59
|
+
const id = parseId(token.args[0]);
|
|
60
|
+
if (id === null)
|
|
61
|
+
return { applied: false, error: `todo:set 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
62
|
+
const status = token.args[1]?.trim();
|
|
63
|
+
if (!status || !(status === "pending" || status === "in_progress" || status === "completed")) {
|
|
64
|
+
return { applied: false, error: `todo:set 状态无效: "${status ?? ""}",应为 pending|in_progress|completed` };
|
|
65
|
+
}
|
|
66
|
+
const task = findTask(state, id);
|
|
67
|
+
if (!task)
|
|
68
|
+
return { applied: false, error: `todo:set 任务 #${id} 不存在` };
|
|
69
|
+
const activeForm = token.kwargs["activeForm"];
|
|
70
|
+
const from = task.status;
|
|
71
|
+
if (status === "pending" && from === "completed") {
|
|
72
|
+
return { applied: false, error: `任务 #${id} 已完成,不能置回 pending` };
|
|
73
|
+
}
|
|
74
|
+
if (status === "in_progress" && from === "completed") {
|
|
75
|
+
return { applied: false, error: `任务 #${id} 已完成,不能重新进行中` };
|
|
76
|
+
}
|
|
77
|
+
task.status = status;
|
|
78
|
+
if (status === "in_progress" && activeForm)
|
|
79
|
+
task.activeForm = activeForm;
|
|
80
|
+
const change = from !== status ? ` (${from} → ${status})` : "";
|
|
81
|
+
return { applied: true, feedback: `Updated #${id}${change}` };
|
|
82
|
+
}
|
|
83
|
+
case "remove": {
|
|
84
|
+
const id = parseId(token.args[0]);
|
|
85
|
+
if (id === null)
|
|
86
|
+
return { applied: false, error: `todo:remove 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
87
|
+
const task = findTask(state, id);
|
|
88
|
+
if (!task)
|
|
89
|
+
return { applied: false, error: `todo:remove 任务 #${id} 不存在` };
|
|
90
|
+
task.status = "deleted";
|
|
91
|
+
return { applied: true, feedback: `Deleted #${id}: ${task.subject}` };
|
|
92
|
+
}
|
|
93
|
+
case "dep": {
|
|
94
|
+
const id = parseId(token.args[0]);
|
|
95
|
+
if (id === null)
|
|
96
|
+
return { applied: false, error: `todo:dep 的 id 无效: "${token.args[0] ?? ""}"` };
|
|
97
|
+
const task = findTask(state, id);
|
|
98
|
+
if (!task)
|
|
99
|
+
return { applied: false, error: `todo:dep 任务 #${id} 不存在` };
|
|
100
|
+
const depRaw = token.kwargs["blocks"] ?? token.args[1] ?? "";
|
|
101
|
+
const deps = depRaw
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((x) => parseId(x.trim()))
|
|
104
|
+
.filter((x) => x !== null);
|
|
105
|
+
const bad = deps.filter((d) => d === id || !findTask(state, d));
|
|
106
|
+
if (bad.length)
|
|
107
|
+
return { applied: false, error: `todo:dep 检测到非法依赖 ${bad.join(",")}(不存在或自环)` };
|
|
108
|
+
task.blockedBy = deps;
|
|
109
|
+
return { applied: true, feedback: `#${id} blocks: ${deps.length ? deps.join(",") : "(none)"}` };
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
return { applied: false, error: `todo 未知操作: ${op}` };
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
overlay(state) {
|
|
116
|
+
if (!state || state.tasks.length === 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
const visible = state.tasks.filter((t) => t.status !== "deleted");
|
|
119
|
+
if (visible.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
const done = visible.filter((t) => t.status === "completed").length;
|
|
122
|
+
const lines = visible.map((t) => {
|
|
123
|
+
const mark = t.status === "completed" ? "✓" : t.status === "in_progress" ? "◐" : "○";
|
|
124
|
+
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
125
|
+
return ` ${mark} #${t.id} ${t.subject}${form}`;
|
|
126
|
+
});
|
|
127
|
+
return { tool: "todo", lines: [`${done}/${visible.length} done`, ...lines] };
|
|
128
|
+
},
|
|
129
|
+
init: initTodoState,
|
|
130
|
+
};
|