@aalis/plugin-commands 0.14.0 → 0.15.1
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/dist/commands.d.ts +21 -3
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +94 -37
- package/dist/commands.js.map +1 -1
- package/dist/help.d.ts +13 -4
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +88 -45
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +9 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -235
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts +62 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +62 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/texts.d.ts +123 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +65 -0
- package/dist/texts.js.map +1 -0
- package/package.json +21 -15
package/dist/index.js
CHANGED
|
@@ -1,86 +1,40 @@
|
|
|
1
|
+
import { agent } from '@aalis/api-agent';
|
|
1
2
|
import { authority } from '@aalis/api-authority';
|
|
2
3
|
import { commands as commandsService } from '@aalis/api-commands';
|
|
4
|
+
import { contributions } from '@aalis/api-contributions';
|
|
3
5
|
import { gateway, INBOUND_PHASE } from '@aalis/api-gateway';
|
|
4
6
|
import { hooks } from '@aalis/api-hooks';
|
|
7
|
+
import { i18n } from '@aalis/api-i18n';
|
|
5
8
|
import { memory } from '@aalis/api-memory';
|
|
6
|
-
import { isStorageNotFound, storage } from '@aalis/api-storage';
|
|
7
9
|
import { tools } from '@aalis/api-tools';
|
|
8
|
-
import { appService, config, definePlugin, events, lifecycle, logger, optional,
|
|
10
|
+
import { appService, config, definePlugin, events, lifecycle, logger, optional, pluginsService, provide, } from '@aalis/core';
|
|
9
11
|
import { defineConfig, parseConfig } from '@aalis/schema-config';
|
|
12
|
+
import { isTextRef, TextError, textLines, textOf, toSource } from '@aalis/schema-i18n';
|
|
13
|
+
import { AT_SELF_PATTERN } from '@aalis/schema-message';
|
|
10
14
|
import { CommandRegistry } from './commands.js';
|
|
15
|
+
import { t } from './texts.js';
|
|
11
16
|
const configSchema = defineConfig({
|
|
12
17
|
commandPrefix: {
|
|
13
18
|
type: 'string',
|
|
14
|
-
label: '
|
|
19
|
+
label: t('prefixLabel'),
|
|
15
20
|
default: '/',
|
|
16
21
|
onInvalid: 'error',
|
|
17
|
-
description: '
|
|
22
|
+
description: t('prefixDescription'),
|
|
18
23
|
},
|
|
19
24
|
});
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
* 目录不存在返回 -1。
|
|
23
|
-
*/
|
|
24
|
-
async function removeDirCounted(storage, dirUri) {
|
|
25
|
-
try {
|
|
26
|
-
const st = await storage.stat(dirUri);
|
|
27
|
-
if (!st.isDirectory)
|
|
28
|
-
return -1;
|
|
29
|
-
const list = await storage.list(dirUri);
|
|
30
|
-
await storage.delete(dirUri);
|
|
31
|
-
return list.entries.length;
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
if (isStorageNotFound(err))
|
|
35
|
-
return -1;
|
|
36
|
-
throw err;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** 可清理类型。globalOnly:跨会话数据,只在 /clear all 时清理 */
|
|
40
|
-
export const CLEAR_TYPES = [
|
|
41
|
-
{ id: 'context', label: '消息历史与会话上下文(含摘要与待办)' },
|
|
42
|
-
{ id: 'summary', label: '会话摘要' },
|
|
43
|
-
{ id: 'vector', label: '向量记忆' },
|
|
44
|
-
{ id: 'image', label: '图片缓存' },
|
|
45
|
-
{ id: 'video', label: '视频缓存' },
|
|
46
|
-
{ id: 'audio', label: '语音缓存' },
|
|
47
|
-
{ id: 'file', label: '文件缓存' },
|
|
48
|
-
{ id: 'persona', label: '会话角色状态' },
|
|
49
|
-
{ id: 'checkpoint', label: '检查点(对话回滚存档)' },
|
|
50
|
-
{ id: 'user-profile', label: '用户档案', globalOnly: true },
|
|
51
|
-
{ id: 'user-relation', label: '用户关系图谱', globalOnly: true },
|
|
52
|
-
];
|
|
53
|
-
/**
|
|
54
|
-
* 附件缓存种类 → `data:/` 目录 + 中文标签。
|
|
55
|
-
* 目录名与 @aalis/plugin-adapter-onebot 的 attachment-cache `KIND_DIR` 对齐
|
|
56
|
-
* (image→images / video→videos / audio→audios / file→files),落盘路径为
|
|
57
|
-
* `data:/{dir}/{safeSessionId}/{hash}.{ext}`。改这里即同步 /clear 覆盖面。
|
|
58
|
-
*/
|
|
59
|
-
export const ATTACHMENT_KINDS = [
|
|
60
|
-
{ type: 'image', dir: 'images', label: '图片' },
|
|
61
|
-
{ type: 'video', dir: 'videos', label: '视频' },
|
|
62
|
-
{ type: 'audio', dir: 'audios', label: '语音' },
|
|
63
|
-
{ type: 'file', dir: 'files', label: '文件' },
|
|
64
|
-
];
|
|
65
|
-
const CLEAR_TYPE_ALIASES = {
|
|
66
|
-
history: 'context',
|
|
67
|
-
messages: 'context',
|
|
68
|
-
profile: 'user-profile',
|
|
69
|
-
profiles: 'user-profile',
|
|
70
|
-
};
|
|
71
|
-
function normalizeClearTypes(raw, prefix) {
|
|
25
|
+
/** 按登记表核对 -t 的取值:不给或含 all 时清全部类型(undefined),有不认识的抛给人看的错 */
|
|
26
|
+
function normalizeClearTypes(raw, known, prefix) {
|
|
72
27
|
const values = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
|
|
73
28
|
const types = values
|
|
74
29
|
.flatMap(v => String(v).split(','))
|
|
75
30
|
.map(v => v.trim())
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
.map(v => CLEAR_TYPE_ALIASES[v] ?? v);
|
|
31
|
+
.filter(Boolean);
|
|
78
32
|
if (types.length === 0 || types.includes('all'))
|
|
79
33
|
return undefined;
|
|
80
|
-
const
|
|
81
|
-
const unknown = types.find(t => !known.has(t));
|
|
34
|
+
const unknown = types.find(type => !known.some(k => k.id === type));
|
|
82
35
|
if (unknown) {
|
|
83
|
-
|
|
36
|
+
const ids = ['all', ...known.map(k => k.id)].join(', ');
|
|
37
|
+
throw new TextError(t('unknownClearType', { type: unknown, types: ids, prefix }));
|
|
84
38
|
}
|
|
85
39
|
return [...new Set(types)];
|
|
86
40
|
}
|
|
@@ -93,11 +47,18 @@ const uses = {
|
|
|
93
47
|
*/
|
|
94
48
|
commands: optional(commandsService),
|
|
95
49
|
gateway,
|
|
96
|
-
storage: optional(storage),
|
|
97
50
|
memory: optional(memory),
|
|
51
|
+
/** /clear 清消息历史时经它先停下并按住这些会话;缺席时没有在跑的回合要停 */
|
|
52
|
+
agent: optional(agent),
|
|
98
53
|
authority: optional(authority),
|
|
99
|
-
/** /status
|
|
54
|
+
/** /status 写已登记的工具数 */
|
|
100
55
|
tools: optional(tools),
|
|
56
|
+
/** /status 写插件在跑与出错的个数;缺席时不写这一行 */
|
|
57
|
+
plugins: optional(pluginsService),
|
|
58
|
+
/** /clear 的类型按 memory:clear-type 的登记表取;缺席时只认 all */
|
|
59
|
+
contributions: optional(contributions),
|
|
60
|
+
/** 回复与帮助按这条指令所在会话的语言渲染;缺席时按原文 */
|
|
61
|
+
i18n: optional(i18n),
|
|
101
62
|
app: appService,
|
|
102
63
|
events,
|
|
103
64
|
hooks,
|
|
@@ -106,101 +67,50 @@ const uses = {
|
|
|
106
67
|
logger,
|
|
107
68
|
config,
|
|
108
69
|
provide,
|
|
109
|
-
services,
|
|
110
70
|
};
|
|
111
71
|
export default definePlugin({
|
|
112
72
|
name: '@aalis/plugin-commands',
|
|
113
|
-
displayName: '
|
|
73
|
+
displayName: t('displayName'),
|
|
114
74
|
subsystem: 'core',
|
|
115
75
|
configSchema,
|
|
116
76
|
provides: [commandsService],
|
|
117
77
|
uses,
|
|
118
78
|
apply: registerCommands,
|
|
119
79
|
});
|
|
120
|
-
function registerCommands({ commands, gateway,
|
|
80
|
+
function registerCommands({ commands, gateway, memory, agent, authority, tools, plugins, contributions, i18n, app, events, hooks, lifecycle, logger, config, provide, }) {
|
|
121
81
|
const cfg = parseConfig(configSchema, config.use, logger.use);
|
|
122
82
|
// 创建指令注册表并注册为服务
|
|
123
|
-
const registry = new CommandRegistry(logger.use, authority);
|
|
83
|
+
const registry = new CommandRegistry(logger.use, authority, i18n);
|
|
124
84
|
// 可见性的运行时覆盖(authorityOverrides)现归 authority 配置,不在指令注册表加载。
|
|
125
85
|
// 配置指令系统
|
|
126
86
|
registry.prefix = cfg.commandPrefix;
|
|
127
87
|
// 注册服务
|
|
128
88
|
provide.use(commandsService, registry);
|
|
129
|
-
// ===== 统一 memory:clear 中间件:附件缓存清理(图片/视频/语音/文件)=====
|
|
130
|
-
//
|
|
131
|
-
// 附件按 data:/{images|videos|audios|files}/{safeSessionId} 落盘(与 onebot
|
|
132
|
-
// attachment-cache 的 KIND_DIR 对齐)。独立 middleware 使 /clear 与 deleteSession
|
|
133
|
-
// 走同一清理路径——deleteSession 触发的 memory:clear 也清附件,杜绝文件泄漏
|
|
134
|
-
// (四类必须齐清,漏掉任一类即留下残留文件)。
|
|
135
|
-
//
|
|
136
|
-
// types 语义:未指定=清全部附件;指定则只清命中的种类(如 /clear -t video)。
|
|
137
|
-
hooks.use.middleware('memory:clear', async (data, next) => {
|
|
138
|
-
const safeSessionId = data.sessionId?.replace(/[:/\\]/g, '_');
|
|
139
|
-
for (const k of ATTACHMENT_KINDS) {
|
|
140
|
-
if (data.types && !data.types.includes(k.type))
|
|
141
|
-
continue;
|
|
142
|
-
try {
|
|
143
|
-
if (data.scope === 'all') {
|
|
144
|
-
const removed = await removeDirCounted(storage.use, `data:/${k.dir}`);
|
|
145
|
-
data.results.push({
|
|
146
|
-
source: `${k.type}-cache`,
|
|
147
|
-
type: k.type,
|
|
148
|
-
success: true,
|
|
149
|
-
message: removed >= 0 ? `所有${k.label}缓存已清空(${removed} 个会话目录)` : `${k.label}缓存目录不存在,无需清空`,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
else if (safeSessionId) {
|
|
153
|
-
const removed = await removeDirCounted(storage.use, `data:/${k.dir}/${safeSessionId}`);
|
|
154
|
-
data.results.push({
|
|
155
|
-
source: `${k.type}-cache`,
|
|
156
|
-
type: k.type,
|
|
157
|
-
success: true,
|
|
158
|
-
message: removed >= 0 ? `当前会话${k.label}缓存已清空(${removed} 个)` : `当前会话无${k.label}缓存`,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
164
|
-
data.results.push({
|
|
165
|
-
source: `${k.type}-cache`,
|
|
166
|
-
type: k.type,
|
|
167
|
-
success: false,
|
|
168
|
-
message: `${k.label}缓存清空失败: ${msg}`,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
await next();
|
|
173
|
-
});
|
|
174
89
|
// ===== inbound:command 相位:命令命中则执行并中断后续相位 =====
|
|
175
90
|
//
|
|
176
91
|
// 命令拦截只在 inbound:command 命名相位做,适配器不内联拦截,
|
|
177
92
|
// 所有平台共享同一套命令解析路径。
|
|
178
93
|
// plugin-gateway 的 INBOUND_PHASE_ORDER 把本相位放在 trigger / flow 之前。
|
|
179
94
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const TRUSTED_SOURCE_EXACT = new Set(['scheduler']);
|
|
200
|
-
const isTrustedSystemSource = (source) => !!source && TRUSTED_SOURCE_EXACT.has(source);
|
|
201
|
-
/** 回到消息所在的会话。required 依赖在调度收敛前可能短暂缺席,故保留事件兜底 */
|
|
202
|
-
async function reply(message, content) {
|
|
203
|
-
const out = { content, sessionId: message.sessionId, platform: message.platform, source: 'command' };
|
|
95
|
+
// 只有人亲手写的文字才执行指令。真人入站(平台适配器、WebUI、终端)不带 source,照常解析;带 source 的注入不论
|
|
96
|
+
// source 叫什么,都不过指令解析、当普通消息往下走,只有声明了 ownerApproved 的照常解析:文字在建立时经 owner 过目过原文。
|
|
97
|
+
// 带声明的注入:
|
|
98
|
+
// - 权限身份取创建时固化的 message.actor,守卫按它的真实等级评估,与普通用户走同一闸门。**actor 缺省即匿名**:
|
|
99
|
+
// 创建者匿名时不得回填 owner(在触发路径把空 actor 补成 `webui:console` 之类,等于开了一条 owner 快速通道);
|
|
100
|
+
// - 没人可问,免 session 档的确认(skipConfirm;authorize 仍生效,always 档照常要确认、无从确认即拒);
|
|
101
|
+
// 免确认只对创建者是 owner 的注入生效(执行守卫按授权身份判),别人建的任务里需要确认的指令照旧直接拒;
|
|
102
|
+
// - 通常指向没有适配器接收的 internal 虚拟会话,结果写日志而不回发,避免发到虚空。
|
|
103
|
+
/**
|
|
104
|
+
* 回到消息所在的会话。文字对象交给网关按这个会话的语言渲染,content 写原文。required 依赖在调度收敛前可能短暂缺席,
|
|
105
|
+
* 故保留事件兜底(发出原文)
|
|
106
|
+
*/
|
|
107
|
+
async function reply(message, result) {
|
|
108
|
+
const out = {
|
|
109
|
+
...(isTextRef(result) ? { content: toSource(result), text: result } : { content: result }),
|
|
110
|
+
sessionId: message.sessionId,
|
|
111
|
+
platform: message.platform,
|
|
112
|
+
source: 'command',
|
|
113
|
+
};
|
|
204
114
|
const gatewayService = gateway.present ? gateway.use : undefined;
|
|
205
115
|
if (gatewayService)
|
|
206
116
|
await gatewayService.dispatchOutbound(out);
|
|
@@ -210,7 +120,7 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
210
120
|
// ===== 意图确认:人亲手输入、声明了 confirm 的指令,在同一会话里问发出者本人 =====
|
|
211
121
|
//
|
|
212
122
|
// 只认这个人在这个会话里的下一条消息(带 source 的注入不认):y 为确认,其余取消,这条回复吞掉、不往下走;
|
|
213
|
-
// 时限内不回也取消。问的是「确定要这么做吗」,不是 owner
|
|
123
|
+
// 时限内不回也取消。问的是「确定要这么做吗」,不是 owner 审批:非 owner 的发出者本人确认即可。
|
|
214
124
|
const INTENT_TIMEOUT_MS = 60_000;
|
|
215
125
|
const YES = /^\s*(?:y|yes)\s*$/i;
|
|
216
126
|
/** 等回复的意图确认:键是会话与发出者,值是结算函数 */
|
|
@@ -228,7 +138,7 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
228
138
|
const timer = setTimeout(() => settle(false), INTENT_TIMEOUT_MS);
|
|
229
139
|
timer.unref?.();
|
|
230
140
|
intents.set(key, settle);
|
|
231
|
-
void reply(message,
|
|
141
|
+
void reply(message, t('confirmIntent', { command: raw, seconds: INTENT_TIMEOUT_MS / 1000 }));
|
|
232
142
|
});
|
|
233
143
|
}
|
|
234
144
|
// 停用(含重载)时等待中的确认按取消结算:新实例不认得它们,留到时限只会在拆卸之后才回「已取消」。
|
|
@@ -239,41 +149,46 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
239
149
|
}, '取消等待中的意图确认');
|
|
240
150
|
hooks.use.middleware(INBOUND_PHASE.COMMAND, async (data, next) => {
|
|
241
151
|
const { message } = data;
|
|
242
|
-
const
|
|
152
|
+
const injected = message.source !== undefined;
|
|
153
|
+
// 开头对她的 @(AT_SELF_PATTERN,只认开头)不算正文,意图确认的回复与指令同用这一份:在群里 @ 她或引用她的消息发指令、
|
|
154
|
+
// 回 y 时,QQ 客户端会在开头插一个。开头 @ 的是别人时照旧不算
|
|
155
|
+
const content = message.content ?? '';
|
|
156
|
+
const at = AT_SELF_PATTERN.exec(content);
|
|
157
|
+
const text = at?.index === 0 ? content.slice(at[0].length) : content;
|
|
158
|
+
const settle = injected ? undefined : intents.get(intentKey(message));
|
|
243
159
|
if (settle) {
|
|
244
|
-
settle(YES.test(
|
|
160
|
+
settle(YES.test(text));
|
|
245
161
|
return;
|
|
246
162
|
}
|
|
247
|
-
//
|
|
248
|
-
if (!message.
|
|
163
|
+
// 带 source 又没声明 ownerApproved 的注入不是人亲手写的,与没有文字的消息一样不参与命令解析
|
|
164
|
+
if (!content || (injected && !message.ownerApproved))
|
|
249
165
|
return next();
|
|
250
|
-
const parsed = registry.parseCommand(
|
|
166
|
+
const parsed = registry.parseCommand(text);
|
|
251
167
|
if (!parsed)
|
|
252
168
|
return next();
|
|
253
169
|
// 解析到 "<prefix>foo" 但没有任何插件注册过该指令 → 当作普通消息处理
|
|
254
170
|
// (归档、trigger、agent 等下游相位继续工作),避免对错字/打字噪音回显"未知指令"。
|
|
255
171
|
if (!registry.hasMatch(parsed.name, parsed.args))
|
|
256
172
|
return next();
|
|
257
|
-
const isSystemTrigger = isTrustedSystemSource(message.source);
|
|
258
173
|
try {
|
|
259
174
|
// 优先用 actor(系统触发器注入的代理身份),fallback 到消息原始身份。
|
|
260
175
|
// 与 agent 工具调用路径解析调用者身份同语义。
|
|
261
|
-
const result = await registry.
|
|
176
|
+
const result = await registry.run(parsed.name, {
|
|
262
177
|
sessionId: message.sessionId,
|
|
263
178
|
platform: message.actor?.platform ?? message.platform,
|
|
264
179
|
userId: message.actor?.userId ?? message.userId,
|
|
265
180
|
sessionType: message.sessionType,
|
|
266
181
|
args: parsed.args,
|
|
267
182
|
raw: parsed.raw,
|
|
268
|
-
|
|
183
|
+
// 走到这里的注入都声明了 ownerApproved
|
|
184
|
+
skipConfirm: injected,
|
|
269
185
|
// 人亲手输入的消息才问得到发出者
|
|
270
|
-
confirmIntent:
|
|
186
|
+
confirmIntent: !injected && message.userId !== undefined ? askIntent(message, parsed.raw) : undefined,
|
|
271
187
|
});
|
|
272
188
|
if (result) {
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const preview = result.length > 500 ? `${result.slice(0, 500)}…` : result;
|
|
189
|
+
if (injected) {
|
|
190
|
+
const output = toSource(result);
|
|
191
|
+
const preview = output.length > 500 ? `${output.slice(0, 500)}…` : output;
|
|
277
192
|
logger.use.info(`[${message.source}] ${parsed.raw} → session=${message.sessionId} 结果:\n${preview}`);
|
|
278
193
|
}
|
|
279
194
|
else {
|
|
@@ -289,85 +204,88 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
289
204
|
// ===== 内置指令 =====
|
|
290
205
|
// 只列调用者能用的指令:用不了的在帮助里当作不存在
|
|
291
206
|
commands.use
|
|
292
|
-
.command('help [name:text]', '
|
|
293
|
-
.argument('name', '
|
|
207
|
+
.command('help [name:text]', t('helpDescription'))
|
|
208
|
+
.argument('name', t('helpName'))
|
|
294
209
|
.action(async (argv, name) => {
|
|
295
|
-
const caller = { platform: argv.session.platform, userId: argv.session.userId };
|
|
296
210
|
const path = String(name ?? '')
|
|
297
211
|
.split(/[\s.]+/)
|
|
298
212
|
.filter(Boolean);
|
|
299
|
-
return path.length > 0 ? registry.detail(path,
|
|
213
|
+
return path.length > 0 ? registry.detail(path, argv.session) : registry.overview(argv.session);
|
|
300
214
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
['
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
];
|
|
315
|
-
for (const [label, ok] of checks) {
|
|
316
|
-
lines.push(`- ${label}: ${ok ? '✅ 可用' : '❌ 不可用'}`);
|
|
317
|
-
}
|
|
318
|
-
lines.push(`- 已注册工具: ${tools.present ? tools.use.getAll().length : 0} 个`);
|
|
319
|
-
lines.push(`- 已注册指令: ${registry.getAll().length} 个`);
|
|
320
|
-
return lines.join('\n');
|
|
215
|
+
// 只给计数:逐项的检查(哪个插件为什么无法启动、缺什么服务)交给 /doctor,末尾指过去
|
|
216
|
+
commands.use.command('status', t('statusDescription')).action(async () => {
|
|
217
|
+
const status = plugins.present ? plugins.use.getStatus() : undefined;
|
|
218
|
+
const count = (state) => status?.filter(p => p.state === state).length ?? 0;
|
|
219
|
+
return textLines([
|
|
220
|
+
t('statusTitle'),
|
|
221
|
+
'',
|
|
222
|
+
...(status ? [t('statusPlugins', { active: count('active'), error: count('error') })] : []),
|
|
223
|
+
t('statusTools', { count: tools.present ? tools.use.getAll().length : 0 }),
|
|
224
|
+
t('statusCommands', { count: registry.getAll().length }),
|
|
225
|
+
'',
|
|
226
|
+
t('statusDoctor', { prefix: registry.prefix }),
|
|
227
|
+
]);
|
|
321
228
|
});
|
|
322
|
-
commands.use.command('shutdown', '
|
|
229
|
+
commands.use.command('shutdown', t('shutdownDescription'), { visibility: 'restricted' }).action(async () => {
|
|
323
230
|
const host = app.use;
|
|
324
231
|
setTimeout(async () => {
|
|
325
232
|
await host.stop();
|
|
326
233
|
process.exit(0);
|
|
327
234
|
}, 500);
|
|
328
|
-
return '
|
|
235
|
+
return t('shuttingDown');
|
|
329
236
|
});
|
|
330
|
-
commands.use.command('restart', '
|
|
237
|
+
commands.use.command('restart', t('restartDescription'), { visibility: 'restricted' }).action(async () => {
|
|
331
238
|
app.use.restart();
|
|
332
|
-
return '
|
|
239
|
+
return t('restarting');
|
|
333
240
|
});
|
|
334
|
-
|
|
241
|
+
// ===== /clear —— 清空记忆 =====
|
|
242
|
+
// 默认清当前会话;全局清理通过显式危险子指令 /clear all 进入,避免把高危语义藏在普通选项里。
|
|
243
|
+
// 类型由处理它的插件登记(本插件登记它清的消息历史 context),-t 的可选项按登记表现取,同一类型只列一次。
|
|
244
|
+
contributions.use.contribute('memory:clear-type', { id: 'context', label: t('typeContext') });
|
|
245
|
+
/** 登记的清理类型,按收集次序去重:同一类型几家登记时取第一家的显示名 */
|
|
246
|
+
function clearTypes() {
|
|
247
|
+
if (!contributions.present)
|
|
248
|
+
return [];
|
|
249
|
+
const byId = new Map();
|
|
250
|
+
for (const { spec } of contributions.use.collect('memory:clear-type'))
|
|
251
|
+
if (!byId.has(spec.id))
|
|
252
|
+
byId.set(spec.id, spec);
|
|
253
|
+
return [...byId.values()];
|
|
254
|
+
}
|
|
255
|
+
async function runClear(cmdCtx, scope, types, known) {
|
|
335
256
|
const isGlobal = scope === 'all';
|
|
336
257
|
// 预检:消息历史清不了就整条不执行。各插件的 memory:clear 中间件先于消息清理运行,放行就会清掉摘要、待办、
|
|
337
258
|
// 附件等,留下「消息还在、其余已清」的半截状态。预检只决定执行与否,清理时另取当时的记忆后端
|
|
338
259
|
const clearsMessages = !types || types.includes('context');
|
|
339
260
|
const memoryAtStart = memory.present ? memory.use : undefined;
|
|
340
|
-
if (clearsMessages && !memoryAtStart)
|
|
341
|
-
return ('
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (clearsMessages && isGlobal && !memoryAtStart?.clearAll) {
|
|
345
|
-
return '⚠ 记忆后端不支持全局清空消息历史,本次未清理任何内容。可用 -t 指定 context 以外的类型单独清理。';
|
|
346
|
-
}
|
|
261
|
+
if (clearsMessages && !memoryAtStart)
|
|
262
|
+
return t('noMemoryBackend');
|
|
263
|
+
if (clearsMessages && isGlobal && !memoryAtStart?.clearAll)
|
|
264
|
+
return t('noGlobalClear');
|
|
347
265
|
const clearData = {
|
|
348
266
|
scope,
|
|
349
267
|
types,
|
|
350
268
|
sessionId: cmdCtx.sessionId,
|
|
351
269
|
results: [],
|
|
352
270
|
};
|
|
353
|
-
|
|
271
|
+
const run = () => hooks.use.run('memory:clear', clearData, async () => {
|
|
354
272
|
if (!clearsMessages)
|
|
355
273
|
return;
|
|
356
274
|
try {
|
|
357
275
|
// 中间件链可能要等一阵(在途落库、向量清空与保存),期间 memory 胜者可能已更换:按此刻的胜者清理,
|
|
358
276
|
// 不复用预检取到的实例,免得清到已下线的旧后端
|
|
359
277
|
if (!memory.present)
|
|
360
|
-
throw new
|
|
278
|
+
throw new TextError(t('memoryGone'));
|
|
361
279
|
const memoryService = memory.use;
|
|
362
280
|
if (isGlobal) {
|
|
363
281
|
if (!memoryService.clearAll)
|
|
364
|
-
throw new
|
|
282
|
+
throw new TextError(t('noGlobalClearMidway'));
|
|
365
283
|
await memoryService.clearAll();
|
|
366
284
|
clearData.results.push({
|
|
367
285
|
source: 'memory',
|
|
368
286
|
type: 'context',
|
|
369
287
|
success: true,
|
|
370
|
-
message: '
|
|
288
|
+
message: t('historyClearedAll'),
|
|
371
289
|
});
|
|
372
290
|
}
|
|
373
291
|
else {
|
|
@@ -376,71 +294,57 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
376
294
|
source: 'memory',
|
|
377
295
|
type: 'context',
|
|
378
296
|
success: true,
|
|
379
|
-
message: '
|
|
297
|
+
message: t('historyClearedSession'),
|
|
380
298
|
});
|
|
381
299
|
}
|
|
382
300
|
}
|
|
383
301
|
catch (err) {
|
|
384
|
-
|
|
385
|
-
|
|
302
|
+
clearData.results.push({
|
|
303
|
+
source: 'memory',
|
|
304
|
+
type: 'context',
|
|
305
|
+
success: false,
|
|
306
|
+
message: t('clearFailed', { error: textOf(err) }),
|
|
307
|
+
});
|
|
386
308
|
}
|
|
387
|
-
//
|
|
388
|
-
// 统一由各自的 memory:clear middleware 处理(见上面的附件缓存 middleware)。
|
|
309
|
+
// 附件、向量、人设、用户档案等子系统的清理由各自的 memory:clear 中间件处理(附件归写入它的插件)。
|
|
389
310
|
// runClear 仅负责调度 hook 与处理 memory 主体。
|
|
390
311
|
});
|
|
391
|
-
|
|
312
|
+
// 清消息历史时经 agent 先停下这些会话在跑的一轮、按住它们,再跑整条清理链:停下与按住在各家清理之外,不看它们的登记先后
|
|
313
|
+
await (clearsMessages && agent.present
|
|
314
|
+
? agent.use.clearing(isGlobal ? undefined : [cmdCtx.sessionId], run)
|
|
315
|
+
: run());
|
|
316
|
+
// 每行就是各家的说明,成败已写在说明里
|
|
317
|
+
const lines = clearData.results.map(r => r.message);
|
|
392
318
|
// 显式要求的类型逐个核对:没有任何一行标注该类型,就是没有已启用的插件处理它,照实说明
|
|
393
|
-
for (const
|
|
394
|
-
if (!types?.includes(
|
|
319
|
+
for (const { id, label } of known) {
|
|
320
|
+
if (!types?.includes(id) || clearData.results.some(r => r.type === id))
|
|
395
321
|
continue;
|
|
396
|
-
lines.push(
|
|
397
|
-
? `⚠ ${t.label}只在 ${registry.prefix}clear all 时清理,本次未清理`
|
|
398
|
-
: `⚠ ${t.label}:没有已启用的插件处理这一类型,未清理`);
|
|
322
|
+
lines.push(t('unhandledType', { type: label }));
|
|
399
323
|
}
|
|
400
|
-
|
|
401
|
-
return '无可清除的记忆模块。';
|
|
402
|
-
return lines.join('\n');
|
|
324
|
+
return lines.length === 0 ? t('nothingCleared') : textLines(lines);
|
|
403
325
|
}
|
|
404
|
-
/**
|
|
405
|
-
* 清空共享会话(群/频道)所需的最低等级。私聊会话归用户本人,不受此限——
|
|
406
|
-
* 清自己的记忆是自助行为;群会话一人清掉会毁掉所有人的上下文,故要求同
|
|
407
|
-
* `visibility:'restricted'` 的等级 2。
|
|
408
|
-
*/
|
|
409
|
-
const CLEAR_SHARED_MIN_LEVEL = 2;
|
|
410
326
|
async function runClearFromOptions(argv, scope) {
|
|
327
|
+
const known = clearTypes();
|
|
411
328
|
let types;
|
|
412
329
|
try {
|
|
413
|
-
types = normalizeClearTypes(argv.options.type, registry.prefix);
|
|
330
|
+
types = normalizeClearTypes(argv.options.type, known, registry.prefix);
|
|
414
331
|
}
|
|
415
332
|
catch (err) {
|
|
416
|
-
return
|
|
417
|
-
}
|
|
418
|
-
// 共享会话设防:静态声明只能给出与会话归属无关的策略,这一档必须在运行期判。
|
|
419
|
-
if (argv.session.sessionType && argv.session.sessionType !== 'private') {
|
|
420
|
-
const auth = authority.present ? authority.use : undefined;
|
|
421
|
-
const isOwner = auth?.isOwner(argv.session.platform, argv.session.userId) ?? false;
|
|
422
|
-
const level = auth?.listUsers().find(u => u.platform === argv.session.platform && u.userId === argv.session.userId)?.level ??
|
|
423
|
-
0;
|
|
424
|
-
if (!isOwner && level < CLEAR_SHARED_MIN_LEVEL) {
|
|
425
|
-
return `清空共享会话需要等级 ${CLEAR_SHARED_MIN_LEVEL}(当前 ${level})。私聊里可以随时清理自己的会话。`;
|
|
426
|
-
}
|
|
333
|
+
return textOf(err);
|
|
427
334
|
}
|
|
428
|
-
return runClear({ sessionId: argv.session.sessionId }, scope, types);
|
|
335
|
+
return runClear({ sessionId: argv.session.sessionId }, scope, types, known);
|
|
429
336
|
}
|
|
430
337
|
const clearTypeOption = {
|
|
431
|
-
description: '
|
|
432
|
-
choices: [
|
|
433
|
-
{ value: 'all', description: '
|
|
434
|
-
...
|
|
338
|
+
description: t('clearTypeDescription'),
|
|
339
|
+
choices: () => [
|
|
340
|
+
{ value: 'all', description: t('clearTypeAll') },
|
|
341
|
+
...clearTypes().map(({ id, label }) => ({ value: id, description: label })),
|
|
435
342
|
],
|
|
436
343
|
};
|
|
437
344
|
commands.use
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// 群聊会话是共享的,一人清掉毁掉所有人的上下文——那一档在 action 里按
|
|
442
|
-
// sessionType 另行设防(与 /bind 等私聊敏感指令同一模式)。
|
|
443
|
-
.command('clear', '清空当前会话记忆;用 --type 选择消息、摘要、向量、图片等清理类型', {
|
|
345
|
+
// 清空**当前会话**的记忆;确认挡住提示词注入触发的误清(需真人回一句)。
|
|
346
|
+
.command('clear', t('clearDescription'), {
|
|
347
|
+
risk: 'sensitive',
|
|
444
348
|
confirm: 'session',
|
|
445
349
|
})
|
|
446
350
|
.option('type', '-t <type:string[]>', clearTypeOption)
|
|
@@ -452,7 +356,7 @@ function registerCommands({ commands, gateway, storage, memory, authority, tools
|
|
|
452
356
|
commands.use
|
|
453
357
|
// 全局清空,比 /clear 更重:同样 dangerous(等级 2 + 二次确认)。
|
|
454
358
|
// 原先只写 visibility:'restricted' 拿到了等级 2 但漏了 confirm。
|
|
455
|
-
.command('clear.all', '
|
|
359
|
+
.command('clear.all', t('clearAllDescription'), {
|
|
456
360
|
risk: 'dangerous',
|
|
457
361
|
})
|
|
458
362
|
.option('type', '-t <type:string[]>', clearTypeOption)
|