agent-loft 0.2.30 → 0.2.32
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 +11 -1
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +13 -8
- package/src/agents/claude/session.ts +38 -6
- package/src/agents/codex/session.ts +32 -5
- package/src/channels/feishu/bot.ts +8 -4
- package/src/channels/feishu/turn-stream.ts +3 -2
- package/src/channels/web/api.ts +8 -0
- package/src/channels/web/http.ts +2 -2
- package/src/channels/web/server.ts +21 -1
- package/src/channels/web/skills-api.ts +225 -0
- package/src/channels/web/tasks-api.ts +94 -0
- package/src/core/chat-mode.ts +16 -16
- package/src/core/command-catalog.ts +8 -1
- package/src/core/commands.ts +3 -0
- package/src/core/hub.ts +147 -19
- package/src/core/memory-mcp.ts +1 -1
- package/src/core/personal.ts +5 -1
- package/src/core/skill-runtime.ts +359 -0
- package/src/core/skills-bridge.ts +288 -0
- package/src/core/skills.ts +675 -0
- package/src/core/store.ts +2 -0
- package/src/core/task-prompt.ts +38 -0
- package/src/core/task-runner.ts +232 -0
- package/src/core/task-schedule.ts +250 -0
- package/src/core/tasks-bridge.ts +256 -0
- package/src/core/tasks-mcp.ts +66 -0
- package/src/core/tasks.ts +281 -0
- package/src/core/types.ts +21 -0
- package/src/core/zip.ts +98 -0
- package/src/main.ts +12 -0
- package/src/mcp/tasks-server.ts +327 -0
- package/web/css/skills.css +368 -0
- package/web/css/tasks.css +455 -0
- package/web/index.html +2 -0
- package/web/lib/dom.js +1 -0
- package/web/lib/ws.js +8 -0
- package/web/state.js +2 -0
- package/web/views/composer.js +15 -3
- package/web/views/conv-actions.js +15 -7
- package/web/views/layout.js +22 -1
- package/web/views/mention.js +193 -0
- package/web/views/messages.js +23 -2
- package/web/views/sidebar.js +4 -0
- package/web/views/skills.js +703 -0
- package/web/views/tasks.js +469 -0
- package/web/views/topbar.js +9 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// 技能页面的接口:/api/skills。看得到的技能(自己的、公开的、指名给自己的)、安装 / 停用、变量、上传。
|
|
2
|
+
// 只有作者能改;管理员能删别人公开或分享出来的(下架),但看不到、删不了别人私有的。
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
import { machineSkills, mentionTargets } from "../../core/skill-runtime.js";
|
|
7
|
+
import {
|
|
8
|
+
canDelete,
|
|
9
|
+
canSee,
|
|
10
|
+
createSkill,
|
|
11
|
+
declaredHosts,
|
|
12
|
+
deleteSkill,
|
|
13
|
+
getSkill,
|
|
14
|
+
installCount,
|
|
15
|
+
installSkill,
|
|
16
|
+
listAllSkills,
|
|
17
|
+
listFiles,
|
|
18
|
+
parseSkillMd,
|
|
19
|
+
prepareUpload,
|
|
20
|
+
readSkillFile,
|
|
21
|
+
readSkillMd,
|
|
22
|
+
setSkillEnabled,
|
|
23
|
+
setSkillVars,
|
|
24
|
+
SKILL_LIMITS,
|
|
25
|
+
updateSkill,
|
|
26
|
+
userPrefs,
|
|
27
|
+
type Skill,
|
|
28
|
+
type SkillInput,
|
|
29
|
+
} from "../../core/skills.js";
|
|
30
|
+
import { readZip } from "../../core/zip.js";
|
|
31
|
+
import { getUser, isAdmin, listUsers, type User } from "../../core/users.js";
|
|
32
|
+
import { HttpError, readJsonBody, sendJson } from "./http.js";
|
|
33
|
+
|
|
34
|
+
function view(s: Skill, user: User) {
|
|
35
|
+
const prefs = userPrefs(user.id);
|
|
36
|
+
const mine = s.owner === user.id;
|
|
37
|
+
return {
|
|
38
|
+
id: s.id,
|
|
39
|
+
name: s.name,
|
|
40
|
+
description: s.description,
|
|
41
|
+
owner: s.owner,
|
|
42
|
+
ownerName: getUser(s.owner)?.name ?? "(已删除的用户)",
|
|
43
|
+
mine,
|
|
44
|
+
visibility: s.visibility,
|
|
45
|
+
// 分享给了谁只有作者看得到
|
|
46
|
+
...(mine ? { sharedWith: s.sharedWith, installs: installCount(s.id) } : {}),
|
|
47
|
+
vars: s.vars,
|
|
48
|
+
source: s.source,
|
|
49
|
+
createdAt: s.createdAt,
|
|
50
|
+
updatedAt: s.updatedAt,
|
|
51
|
+
installed: mine || prefs.installed.includes(s.id),
|
|
52
|
+
enabled: !prefs.disabled.includes(s.id),
|
|
53
|
+
canDelete: canDelete({ id: user.id, role: user.role }, s),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 变量:值按名字存;密钥(任何一个看得到的技能把它标成密钥)不回显,只说填没填。
|
|
59
|
+
* 受保护的密钥再带上确认过的地址(hosts),作者后来加了没确认过的地址就列在 pending 里。
|
|
60
|
+
*/
|
|
61
|
+
function varsView(user: User, skills: Skill[]) {
|
|
62
|
+
const secret = new Set(skills.flatMap((s) => s.vars.filter((v) => v.secret).map((v) => v.name)));
|
|
63
|
+
const prefs = userPrefs(user.id);
|
|
64
|
+
const mineOrInstalled = skills.filter((s) => s.owner === user.id || prefs.installed.includes(s.id));
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
Object.entries(prefs.vars).map(([k, v]) => {
|
|
67
|
+
const declared = declaredHosts(mineOrInstalled, k);
|
|
68
|
+
const approved = prefs.approved[k] ?? [];
|
|
69
|
+
const pending = declared.filter((h) => !approved.includes(h));
|
|
70
|
+
return [
|
|
71
|
+
k,
|
|
72
|
+
{
|
|
73
|
+
set: true,
|
|
74
|
+
...(secret.has(k) ? {} : { value: v }),
|
|
75
|
+
...(declared.length ? { hosts: approved } : {}),
|
|
76
|
+
...(pending.length ? { pending } : {}),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type UploadBody = { zip?: string; files?: { path?: string; data?: string }[] };
|
|
84
|
+
|
|
85
|
+
/** 上传:zip(.zip / .skill)或者一堆文件(选文件夹 / 单个 SKILL.md),内容都是 base64 */
|
|
86
|
+
function uploadOf(raw: UploadBody | undefined): ReturnType<typeof prepareUpload> | undefined {
|
|
87
|
+
if (!raw) return undefined;
|
|
88
|
+
if (typeof raw.zip === "string") {
|
|
89
|
+
const entries = readZip(Buffer.from(raw.zip, "base64"), SKILL_LIMITS.totalBytes * 2, SKILL_LIMITS.files * 2);
|
|
90
|
+
return prepareUpload(entries);
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(raw.files)) {
|
|
93
|
+
if (raw.files.length > SKILL_LIMITS.files * 2) throw new Error(`文件太多(最多 ${SKILL_LIMITS.files} 个)`);
|
|
94
|
+
return prepareUpload(raw.files.map((f) => ({ path: String(f.path ?? ""), data: Buffer.from(String(f.data ?? ""), "base64") })));
|
|
95
|
+
}
|
|
96
|
+
throw new Error("上传的内容不对");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const bad = (err: unknown) => (err instanceof HttpError ? err : new HttpError(400, err instanceof Error ? err.message : String(err)));
|
|
100
|
+
|
|
101
|
+
/** 处理了就返回 true */
|
|
102
|
+
export async function handleSkillsApi(req: IncomingMessage, res: ServerResponse, url: URL, user: User): Promise<boolean> {
|
|
103
|
+
const method = req.method ?? "GET";
|
|
104
|
+
const p = url.pathname;
|
|
105
|
+
const viewer = { id: user.id, role: user.role };
|
|
106
|
+
|
|
107
|
+
if (p === "/api/skills" && method === "GET") {
|
|
108
|
+
const visible = listAllSkills().filter((s) => canSee(viewer, s));
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
skills: visible.map((s) => view(s, user)),
|
|
111
|
+
vars: varsView(user, visible),
|
|
112
|
+
// 分享给谁:列出其他还能登录的用户
|
|
113
|
+
users: listUsers()
|
|
114
|
+
.filter((u) => !u.disabled && u.id !== user.id)
|
|
115
|
+
.map((u) => ({ id: u.id, name: u.name })),
|
|
116
|
+
...(isAdmin(user) ? { machine: machineSkills().map((m) => ({ name: m.name, description: m.description, dir: m.dir, agents: m.agents })) } : {}),
|
|
117
|
+
limits: SKILL_LIMITS,
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 输入框 @ 的补全:自己能用的(管理员加上机器上原有的)
|
|
123
|
+
if (p === "/api/skills/mention" && method === "GET") {
|
|
124
|
+
sendJson(res, 200, { skills: mentionTargets(user.id).map((t) => ({ name: t.name, description: t.description, kind: t.kind })) });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 管理员看机器上原有技能的 SKILL.md
|
|
129
|
+
if (p === "/api/skills/machine" && method === "GET") {
|
|
130
|
+
if (!isAdmin(user)) throw new HttpError(403, "只有管理员能看机器上的技能");
|
|
131
|
+
const m = machineSkills().find((x) => x.name === url.searchParams.get("name"));
|
|
132
|
+
if (!m) throw new HttpError(404, "没有这个技能");
|
|
133
|
+
let md = "";
|
|
134
|
+
try {
|
|
135
|
+
md = fs.readFileSync(path.join(m.dir, "SKILL.md"), "utf8");
|
|
136
|
+
} catch {
|
|
137
|
+
md = "(读不到 SKILL.md)";
|
|
138
|
+
}
|
|
139
|
+
sendJson(res, 200, { ...m, skillMd: md });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (p === "/api/skills/vars" && method === "PUT") {
|
|
144
|
+
const body = await readJsonBody<{ values?: Record<string, unknown>; approve?: unknown }>(req);
|
|
145
|
+
try {
|
|
146
|
+
setSkillVars(user.id, body.values ?? {}, Array.isArray(body.approve) ? body.approve.map(String) : []);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw bad(err);
|
|
149
|
+
}
|
|
150
|
+
sendJson(res, 200, { vars: varsView(user, listAllSkills().filter((s) => canSee(viewer, s))) });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (p === "/api/skills" && method === "POST") {
|
|
155
|
+
const body = await readJsonBody<SkillInput & { upload?: UploadBody }>(req, 16 * 1024 * 1024);
|
|
156
|
+
let skill: Skill;
|
|
157
|
+
try {
|
|
158
|
+
const upload = uploadOf(body.upload);
|
|
159
|
+
const { upload: _u, ...input } = body;
|
|
160
|
+
skill = createSkill(user.id, input, upload ? "upload" : "manual", upload);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw bad(err);
|
|
163
|
+
}
|
|
164
|
+
sendJson(res, 200, view(skill, user));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const m = /^\/api\/skills\/(s_[a-f0-9]+)(?:\/(install|enable|file))?$/.exec(p);
|
|
169
|
+
if (!m) return false;
|
|
170
|
+
const skill = getSkill(m[1]!);
|
|
171
|
+
if (!skill || !canSee(viewer, skill)) throw new HttpError(404, "这个技能不存在");
|
|
172
|
+
|
|
173
|
+
if (!m[2] && method === "GET") {
|
|
174
|
+
const md = parseSkillMd(readSkillMd(skill.id));
|
|
175
|
+
sendJson(res, 200, { ...view(skill, user), body: md.body, extra: md.extra, files: listFiles(skill.id) });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (m[2] === "file" && method === "GET") {
|
|
179
|
+
const data = readSkillFile(skill.id, url.searchParams.get("path") ?? "");
|
|
180
|
+
if (!data) throw new HttpError(404, "没有这个文件");
|
|
181
|
+
// 只当文本看:二进制的给个提示
|
|
182
|
+
const text = data.includes(0) ? null : data.toString("utf8");
|
|
183
|
+
sendJson(res, 200, { text, size: data.length });
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (m[2] === "install" && method === "POST") {
|
|
187
|
+
const body = await readJsonBody<{ on?: boolean }>(req);
|
|
188
|
+
try {
|
|
189
|
+
installSkill(user.id, skill.id, body.on !== false);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw bad(err);
|
|
192
|
+
}
|
|
193
|
+
sendJson(res, 200, view(skill, user));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (m[2] === "enable" && method === "POST") {
|
|
197
|
+
const body = await readJsonBody<{ on?: boolean }>(req);
|
|
198
|
+
setSkillEnabled(user.id, skill.id, body.on !== false);
|
|
199
|
+
sendJson(res, 200, view(skill, user));
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (!m[2] && method === "PATCH") {
|
|
203
|
+
const body = await readJsonBody<SkillInput & { upload?: UploadBody }>(req, 16 * 1024 * 1024);
|
|
204
|
+
let updated: Skill;
|
|
205
|
+
try {
|
|
206
|
+
const upload = uploadOf(body.upload);
|
|
207
|
+
const { upload: _u, name: _n, ...input } = body;
|
|
208
|
+
updated = updateSkill(user.id, skill.id, input, upload ? { upload } : {});
|
|
209
|
+
} catch (err) {
|
|
210
|
+
throw bad(err);
|
|
211
|
+
}
|
|
212
|
+
sendJson(res, 200, view(updated, user));
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (!m[2] && method === "DELETE") {
|
|
216
|
+
try {
|
|
217
|
+
deleteSkill(viewer, skill.id);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
throw bad(err);
|
|
220
|
+
}
|
|
221
|
+
sendJson(res, 200, { ok: true });
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 定时任务页面的接口:/api/tasks。每个人只看得到、改得了自己的任务(别人的一律当不存在)。
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { config, resolveWorkspacePath } from "../../core/config.js";
|
|
4
|
+
import { hub } from "../../core/hub.js";
|
|
5
|
+
import { memoryOwnerOf } from "../../core/personal.js";
|
|
6
|
+
import { getConversation } from "../../core/store.js";
|
|
7
|
+
import { forgetTask, isTaskRunning, runTaskNow } from "../../core/task-runner.js";
|
|
8
|
+
import { serverTimeZone } from "../../core/task-schedule.js";
|
|
9
|
+
import { createTask, deleteTask, getTask, listTasks, updateTask, viewOf, type Task, type TaskInput } from "../../core/tasks.js";
|
|
10
|
+
import type { User } from "../../core/users.js";
|
|
11
|
+
import { HttpError, readJsonBody, sendJson } from "./http.js";
|
|
12
|
+
|
|
13
|
+
function view(task: Task) {
|
|
14
|
+
const key = task.target.key;
|
|
15
|
+
const conv = key ? getConversation(key) : undefined;
|
|
16
|
+
return {
|
|
17
|
+
...viewOf(task),
|
|
18
|
+
running: isTaskRunning(task.id),
|
|
19
|
+
// 结果在哪条对话里看(专用对话第一次运行前还没有)
|
|
20
|
+
...(conv && !conv.hidden ? { convKey: conv.key, convTitle: conv.title ?? "" } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 表单 / 请求里的字段先在这里核一遍:agent 要启用着、目录要在工作区里、对话要是自己的 */
|
|
25
|
+
function checkInput(user: User, body: TaskInput): TaskInput {
|
|
26
|
+
const out: TaskInput = { ...body };
|
|
27
|
+
if (body.agent !== undefined) {
|
|
28
|
+
const agent = String(body.agent);
|
|
29
|
+
if (!hub.enabledAgents().some((d) => d.id === agent)) throw new HttpError(400, `${hub.label(agent)} 没有启用`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof body.cwd === "string" && body.cwd.trim()) {
|
|
32
|
+
const cwd = resolveWorkspacePath(body.cwd.trim());
|
|
33
|
+
if (!cwd) throw new HttpError(400, `目录必须存在,且在 ${config().workspaceRoot} 之内`);
|
|
34
|
+
out.cwd = cwd === config().workspaceRoot ? "" : cwd;
|
|
35
|
+
}
|
|
36
|
+
const target = body.target as { kind?: string; key?: string } | undefined;
|
|
37
|
+
if (target?.kind === "conv") {
|
|
38
|
+
const conv = target.key ? getConversation(target.key) : undefined;
|
|
39
|
+
if (!conv || conv.hidden || memoryOwnerOf(conv) !== user.id) throw new HttpError(400, "回复到的那条对话不存在了,改成专用对话吧");
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bad = (err: unknown) => (err instanceof HttpError ? err : new HttpError(400, err instanceof Error ? err.message : String(err)));
|
|
45
|
+
|
|
46
|
+
/** 处理了就返回 true */
|
|
47
|
+
export async function handleTasksApi(req: IncomingMessage, res: ServerResponse, p: string, user: User): Promise<boolean> {
|
|
48
|
+
const method = req.method ?? "GET";
|
|
49
|
+
if (p === "/api/tasks" && method === "GET") {
|
|
50
|
+
sendJson(res, 200, { tasks: listTasks(user.id).map(view), tz: serverTimeZone(), now: Date.now() });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (p === "/api/tasks" && method === "POST") {
|
|
54
|
+
const body = await readJsonBody<TaskInput>(req);
|
|
55
|
+
// 没选 agent(只启用了一个时表单里不显示):用第一个启用着的,不能默认成一个可能没启用的
|
|
56
|
+
if (body.agent === undefined || body.agent === "") body.agent = hub.enabledAgents()[0]?.id;
|
|
57
|
+
let task: Task;
|
|
58
|
+
try {
|
|
59
|
+
task = createTask(user.id, checkInput(user, body), "user");
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw bad(err);
|
|
62
|
+
}
|
|
63
|
+
sendJson(res, 200, view(task));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const m = /^\/api\/tasks\/([^/]+)(?:\/(run))?$/.exec(p);
|
|
67
|
+
if (!m) return false;
|
|
68
|
+
const task = getTask(user.id, decodeURIComponent(m[1]!));
|
|
69
|
+
if (!task) throw new HttpError(404, "这个任务不存在");
|
|
70
|
+
if (m[2] === "run" && method === "POST") {
|
|
71
|
+
const error = runTaskNow(task);
|
|
72
|
+
if (error) throw new HttpError(409, error);
|
|
73
|
+
sendJson(res, 200, view(task));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (!m[2] && method === "PATCH") {
|
|
77
|
+
const body = await readJsonBody<TaskInput>(req);
|
|
78
|
+
let updated: Task;
|
|
79
|
+
try {
|
|
80
|
+
updated = updateTask(user.id, task.id, checkInput(user, body));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw bad(err);
|
|
83
|
+
}
|
|
84
|
+
sendJson(res, 200, view(updated));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (!m[2] && method === "DELETE") {
|
|
88
|
+
deleteTask(user.id, task.id);
|
|
89
|
+
forgetTask(task);
|
|
90
|
+
sendJson(res, 200, { ok: true });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
package/src/core/chat-mode.ts
CHANGED
|
@@ -36,27 +36,27 @@ export function chatSystemPrompt(search: string, memory?: string): string {
|
|
|
36
36
|
const OPEN = "<agent-loft-context>";
|
|
37
37
|
const CLOSE = "</agent-loft-context>";
|
|
38
38
|
|
|
39
|
-
/** Codex:附在会话进程第一条消息后面的背景(模式 +
|
|
40
|
-
export function contextBlock(mode: "chat" | "work", memory: string | undefined, search: string): string {
|
|
39
|
+
/** Codex:附在会话进程第一条消息后面的背景(模式 + 记忆 + 工作模式下的技能清单) */
|
|
40
|
+
export function contextBlock(mode: "chat" | "work", memory: string | undefined, search: string, skills: string[] = []): string {
|
|
41
41
|
const head =
|
|
42
42
|
mode === "chat"
|
|
43
43
|
? ["当前是「聊天模式」,你作为用户的私人 AI 助手和他聊天:", ...chatRules(search)]
|
|
44
44
|
: ["当前是「工作模式」:可以执行命令、读写文件,按平常的方式干活。之前如果说过「聊天模式下不能做」,那个限制已经解除。"];
|
|
45
|
-
return [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
CLOSE,
|
|
52
|
-
].join("\n");
|
|
45
|
+
return wrapContext([...head, ...(memory ? ["", memory] : []), ...(mode === "work" && skills.length ? ["", ...skills] : [])]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 包成 agent-loft 附上的背景:用户看不到(读历史、实时显示时都剥掉),agent 照着做 */
|
|
49
|
+
export function wrapContext(lines: string[]): string {
|
|
50
|
+
return [OPEN, "(这段是 agent-loft 自动附上的背景,不是用户写的。照着做,但不要在回复里提到它。)", "", ...lines, CLOSE].join("\n");
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
/** 剥掉 contextBlock
|
|
53
|
+
/** 剥掉 contextBlock(读历史、列会话标题时用)。一条消息里可能有好几段(定时任务的说明 + Codex 的模式说明) */
|
|
56
54
|
export function stripContext(text: string): string {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
let out = text;
|
|
56
|
+
for (let start = out.indexOf(OPEN); start >= 0; start = out.indexOf(OPEN)) {
|
|
57
|
+
const end = out.indexOf(CLOSE, start);
|
|
58
|
+
const rest = end < 0 ? "" : out.slice(end + CLOSE.length);
|
|
59
|
+
out = `${out.slice(0, start)}${rest}`;
|
|
60
|
+
}
|
|
61
|
+
return out === text ? text : out.trim();
|
|
62
62
|
}
|
|
@@ -24,6 +24,7 @@ export const BUILTIN_COMMANDS: BuiltinSpec[] = [
|
|
|
24
24
|
{ name: "effort", args: "[程度]", description: "切换思考程度" },
|
|
25
25
|
{ name: "chat", description: "切到聊天模式:精简、只能联网搜索,每轮开销小很多" },
|
|
26
26
|
{ name: "work", description: "切到工作模式:agent 原装的全部能力" },
|
|
27
|
+
{ name: "skills", description: "列出能用的技能(消息里写 @名字 点名使用)" },
|
|
27
28
|
{ name: "status", description: "查看当前会话、模型、目录、剩余额度" },
|
|
28
29
|
{ name: "sessions", args: "[all]", description: "列出历史会话,点按钮恢复;加 all 列所有目录" },
|
|
29
30
|
{ name: "resume", args: "<会话ID>", description: "恢复指定会话" },
|
|
@@ -42,7 +43,13 @@ export const BUILTIN_COMMANDS: BuiltinSpec[] = [
|
|
|
42
43
|
];
|
|
43
44
|
|
|
44
45
|
/** agent 自己报上来的命令 */
|
|
45
|
-
export type AgentCommand = {
|
|
46
|
+
export type AgentCommand = {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
args?: string;
|
|
50
|
+
/** agent 自己带的(Claude Code 的 /compact、自带的技能),不是机器上另外装的 */
|
|
51
|
+
builtin?: boolean;
|
|
52
|
+
};
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
55
|
* 用户自定义命令。prompt 里的 $ARGUMENTS 换成命令后面跟的参数;
|
package/src/core/commands.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type ParsedCommand =
|
|
|
20
20
|
| { kind: "autoedit"; on?: boolean }
|
|
21
21
|
| { kind: "mode"; mode: "chat" | "work" }
|
|
22
22
|
| { kind: "help" }
|
|
23
|
+
| { kind: "skills" }
|
|
23
24
|
/** 只发了一个「/」:列出可用命令 */
|
|
24
25
|
| { kind: "commands" }
|
|
25
26
|
| { kind: "approve"; requestId: string; decision: ApprovalDecision }
|
|
@@ -75,6 +76,8 @@ export function parseCommand(raw: string): ParsedCommand {
|
|
|
75
76
|
return arg ? { kind: "chat", text } : { kind: "mode", mode: head.toLowerCase() as "chat" | "work" };
|
|
76
77
|
case "help":
|
|
77
78
|
return { kind: "help" };
|
|
79
|
+
case "skills":
|
|
80
|
+
return arg ? { kind: "chat", text } : { kind: "skills" };
|
|
78
81
|
case "approve": {
|
|
79
82
|
const [requestId, decision] = rest;
|
|
80
83
|
if (!requestId || !decision) return { kind: "chat", text };
|