@aalis/plugin-commands 0.13.1 → 0.15.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/dist/commands.d.ts +50 -13
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +170 -64
- package/dist/commands.js.map +1 -1
- package/dist/help.d.ts +16 -18
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +102 -129
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +16 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +203 -270
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts +61 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +61 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/texts.d.ts +121 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +64 -0
- package/dist/texts.js.map +1 -0
- package/package.json +23 -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 {
|
|
7
|
-
import { appService, config, definePlugin, events, logger, optional,
|
|
9
|
+
import { tools } from '@aalis/api-tools';
|
|
10
|
+
import { appService, config, definePlugin, events, lifecycle, logger, optional, pluginsService, provide, } from '@aalis/core';
|
|
8
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';
|
|
9
14
|
import { CommandRegistry } from './commands.js';
|
|
10
|
-
import {
|
|
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,258 +47,245 @@ 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
|
-
|
|
54
|
+
/** /status 写已登记的工具数 */
|
|
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),
|
|
62
|
+
app: appService,
|
|
100
63
|
events,
|
|
101
64
|
hooks,
|
|
65
|
+
/** 停用时结算等待中的意图确认 */
|
|
66
|
+
lifecycle,
|
|
102
67
|
logger,
|
|
103
68
|
config,
|
|
104
69
|
provide,
|
|
105
|
-
services,
|
|
106
70
|
};
|
|
107
71
|
export default definePlugin({
|
|
108
72
|
name: '@aalis/plugin-commands',
|
|
109
|
-
displayName: '
|
|
73
|
+
displayName: t('displayName'),
|
|
110
74
|
subsystem: 'core',
|
|
111
75
|
configSchema,
|
|
112
76
|
provides: [commandsService],
|
|
113
77
|
uses,
|
|
114
78
|
apply: registerCommands,
|
|
115
79
|
});
|
|
116
|
-
function registerCommands({ commands, gateway,
|
|
117
|
-
const cfg = parseConfig(configSchema, config, logger);
|
|
80
|
+
function registerCommands({ commands, gateway, memory, agent, authority, tools, plugins, contributions, i18n, app, events, hooks, lifecycle, logger, config, provide, }) {
|
|
81
|
+
const cfg = parseConfig(configSchema, config.use, logger.use);
|
|
118
82
|
// 创建指令注册表并注册为服务
|
|
119
|
-
const registry = new CommandRegistry(logger);
|
|
120
|
-
const storageGateway = createStorageGateway(storage);
|
|
83
|
+
const registry = new CommandRegistry(logger.use, authority, i18n);
|
|
121
84
|
// 可见性的运行时覆盖(authorityOverrides)现归 authority 配置,不在指令注册表加载。
|
|
122
85
|
// 配置指令系统
|
|
123
86
|
registry.prefix = cfg.commandPrefix;
|
|
124
87
|
// 注册服务
|
|
125
|
-
provide(commandsService, registry);
|
|
126
|
-
// ===== 统一 memory:clear 中间件:附件缓存清理(图片/视频/语音/文件)=====
|
|
127
|
-
//
|
|
128
|
-
// 附件按 data:/{images|videos|audios|files}/{safeSessionId} 落盘(与 onebot
|
|
129
|
-
// attachment-cache 的 KIND_DIR 对齐)。独立 middleware 使 /clear 与 deleteSession
|
|
130
|
-
// 走同一清理路径——deleteSession 触发的 memory:clear 也清附件,杜绝文件泄漏
|
|
131
|
-
// (四类必须齐清,漏掉任一类即留下残留文件)。
|
|
132
|
-
//
|
|
133
|
-
// types 语义:未指定=清全部附件;指定则只清命中的种类(如 /clear -t video)。
|
|
134
|
-
hooks.middleware('memory:clear', async (data, next) => {
|
|
135
|
-
const safeSessionId = data.sessionId?.replace(/[:/\\]/g, '_');
|
|
136
|
-
for (const k of ATTACHMENT_KINDS) {
|
|
137
|
-
if (data.types && !data.types.includes(k.type))
|
|
138
|
-
continue;
|
|
139
|
-
try {
|
|
140
|
-
if (data.scope === 'all') {
|
|
141
|
-
const removed = await removeDirCounted(storageGateway, `data:/${k.dir}`);
|
|
142
|
-
data.results.push({
|
|
143
|
-
source: `${k.type}-cache`,
|
|
144
|
-
type: k.type,
|
|
145
|
-
success: true,
|
|
146
|
-
message: removed >= 0 ? `所有${k.label}缓存已清空(${removed} 个会话目录)` : `${k.label}缓存目录不存在,无需清空`,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
else if (safeSessionId) {
|
|
150
|
-
const removed = await removeDirCounted(storageGateway, `data:/${k.dir}/${safeSessionId}`);
|
|
151
|
-
data.results.push({
|
|
152
|
-
source: `${k.type}-cache`,
|
|
153
|
-
type: k.type,
|
|
154
|
-
success: true,
|
|
155
|
-
message: removed >= 0 ? `当前会话${k.label}缓存已清空(${removed} 个)` : `当前会话无${k.label}缓存`,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
-
data.results.push({
|
|
162
|
-
source: `${k.type}-cache`,
|
|
163
|
-
type: k.type,
|
|
164
|
-
success: false,
|
|
165
|
-
message: `${k.label}缓存清空失败: ${msg}`,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
await next();
|
|
170
|
-
});
|
|
88
|
+
provide.use(commandsService, registry);
|
|
171
89
|
// ===== inbound:command 相位:命令命中则执行并中断后续相位 =====
|
|
172
90
|
//
|
|
173
91
|
// 命令拦截只在 inbound:command 命名相位做,适配器不内联拦截,
|
|
174
92
|
// 所有平台共享同一套命令解析路径。
|
|
175
93
|
// plugin-gateway 的 INBOUND_PHASE_ORDER 把本相位放在 trigger / flow 之前。
|
|
176
94
|
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
};
|
|
114
|
+
const gatewayService = gateway.present ? gateway.use : undefined;
|
|
115
|
+
if (gatewayService)
|
|
116
|
+
await gatewayService.dispatchOutbound(out);
|
|
117
|
+
else
|
|
118
|
+
await events.use.emit('outbound:message', out);
|
|
119
|
+
}
|
|
120
|
+
// ===== 意图确认:人亲手输入、声明了 confirm 的指令,在同一会话里问发出者本人 =====
|
|
121
|
+
//
|
|
122
|
+
// 只认这个人在这个会话里的下一条消息(带 source 的注入不认):y 为确认,其余取消,这条回复吞掉、不往下走;
|
|
123
|
+
// 时限内不回也取消。问的是「确定要这么做吗」,不是 owner 审批:非 owner 的发出者本人确认即可。
|
|
124
|
+
const INTENT_TIMEOUT_MS = 60_000;
|
|
125
|
+
const YES = /^\s*(?:y|yes)\s*$/i;
|
|
126
|
+
/** 等回复的意图确认:键是会话与发出者,值是结算函数 */
|
|
127
|
+
const intents = new Map();
|
|
128
|
+
const intentKey = (m) => `${m.sessionId}\n${m.platform}\n${m.userId}`;
|
|
129
|
+
function askIntent(message, raw) {
|
|
130
|
+
return () => new Promise(resolve => {
|
|
131
|
+
const key = intentKey(message);
|
|
132
|
+
const settle = (confirmed) => {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
if (intents.get(key) === settle)
|
|
135
|
+
intents.delete(key);
|
|
136
|
+
resolve(confirmed);
|
|
137
|
+
};
|
|
138
|
+
const timer = setTimeout(() => settle(false), INTENT_TIMEOUT_MS);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
intents.set(key, settle);
|
|
141
|
+
void reply(message, t('confirmIntent', { command: raw, seconds: INTENT_TIMEOUT_MS / 1000 }));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// 停用(含重载)时等待中的确认按取消结算:新实例不认得它们,留到时限只会在拆卸之后才回「已取消」。
|
|
145
|
+
// 在撤回登记之前结算,网关还在,取消的回复发得出去
|
|
146
|
+
lifecycle.use.onDrain(() => {
|
|
147
|
+
for (const settle of [...intents.values()])
|
|
148
|
+
settle(false);
|
|
149
|
+
}, '取消等待中的意图确认');
|
|
150
|
+
hooks.use.middleware(INBOUND_PHASE.COMMAND, async (data, next) => {
|
|
200
151
|
const { message } = data;
|
|
201
|
-
|
|
202
|
-
|
|
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));
|
|
159
|
+
if (settle) {
|
|
160
|
+
settle(YES.test(text));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// 带 source 又没声明 ownerApproved 的注入不是人亲手写的,与没有文字的消息一样不参与命令解析
|
|
164
|
+
if (!content || (injected && !message.ownerApproved))
|
|
203
165
|
return next();
|
|
204
|
-
const parsed = registry.parseCommand(
|
|
166
|
+
const parsed = registry.parseCommand(text);
|
|
205
167
|
if (!parsed)
|
|
206
168
|
return next();
|
|
207
169
|
// 解析到 "<prefix>foo" 但没有任何插件注册过该指令 → 当作普通消息处理
|
|
208
170
|
// (归档、trigger、agent 等下游相位继续工作),避免对错字/打字噪音回显"未知指令"。
|
|
209
171
|
if (!registry.hasMatch(parsed.name, parsed.args))
|
|
210
172
|
return next();
|
|
211
|
-
const isSystemTrigger = isTrustedSystemSource(message.source);
|
|
212
173
|
try {
|
|
213
174
|
// 优先用 actor(系统触发器注入的代理身份),fallback 到消息原始身份。
|
|
214
175
|
// 与 agent 工具调用路径解析调用者身份同语义。
|
|
215
|
-
const result = await registry.
|
|
176
|
+
const result = await registry.run(parsed.name, {
|
|
216
177
|
sessionId: message.sessionId,
|
|
217
178
|
platform: message.actor?.platform ?? message.platform,
|
|
218
179
|
userId: message.actor?.userId ?? message.userId,
|
|
219
180
|
sessionType: message.sessionType,
|
|
220
181
|
args: parsed.args,
|
|
221
182
|
raw: parsed.raw,
|
|
222
|
-
|
|
183
|
+
// 走到这里的注入都声明了 ownerApproved
|
|
184
|
+
skipConfirm: injected,
|
|
185
|
+
// 人亲手输入的消息才问得到发出者
|
|
186
|
+
confirmIntent: !injected && message.userId !== undefined ? askIntent(message, parsed.raw) : undefined,
|
|
223
187
|
});
|
|
224
188
|
if (result) {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
logger.info(`[${message.source}] ${parsed.raw} → session=${message.sessionId} 结果:\n${preview}`);
|
|
189
|
+
if (injected) {
|
|
190
|
+
const output = toSource(result);
|
|
191
|
+
const preview = output.length > 500 ? `${output.slice(0, 500)}…` : output;
|
|
192
|
+
logger.use.info(`[${message.source}] ${parsed.raw} → session=${message.sessionId} 结果:\n${preview}`);
|
|
230
193
|
}
|
|
231
194
|
else {
|
|
232
|
-
|
|
233
|
-
const gatewayService = gateway.current;
|
|
234
|
-
const reply = {
|
|
235
|
-
content: result,
|
|
236
|
-
sessionId: message.sessionId,
|
|
237
|
-
platform: message.platform,
|
|
238
|
-
source: 'command',
|
|
239
|
-
};
|
|
240
|
-
if (gatewayService) {
|
|
241
|
-
await gatewayService.dispatchOutbound(reply);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
await events.emit('outbound:message', reply);
|
|
245
|
-
}
|
|
195
|
+
await reply(message, result);
|
|
246
196
|
}
|
|
247
197
|
}
|
|
248
198
|
}
|
|
249
199
|
catch (err) {
|
|
250
|
-
logger.warn(`指令执行失败: ${err}`);
|
|
200
|
+
logger.use.warn(`指令执行失败: ${err}`);
|
|
251
201
|
}
|
|
252
202
|
// 命令命中:不调用 next() —— 整个入站管道立即停止(不再进入 trigger/flow/dispatch)
|
|
253
203
|
});
|
|
254
204
|
// ===== 内置指令 =====
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
// 有参 → 详情。接受空格与点两种写法:`help session set` ≡ `help session.set`
|
|
263
|
-
const path = String(name)
|
|
264
|
-
.trim()
|
|
205
|
+
// 只列调用者能用的指令:用不了的在帮助里当作不存在
|
|
206
|
+
commands.use
|
|
207
|
+
.command('help [name:text]', t('helpDescription'))
|
|
208
|
+
.argument('name', t('helpName'))
|
|
209
|
+
.action(async (argv, name) => {
|
|
210
|
+
const path = String(name ?? '')
|
|
265
211
|
.split(/[\s.]+/)
|
|
266
212
|
.filter(Boolean);
|
|
267
|
-
|
|
268
|
-
if (!cmd)
|
|
269
|
-
return `没有指令 ${path.join(' ')}。敲 ${registry.prefix}help 看可用指令。`;
|
|
270
|
-
const prefixDot = `${cmd.name}.`;
|
|
271
|
-
const children = all.filter(c => c.name.startsWith(prefixDot) && !c.name.slice(prefixDot.length).includes('.'));
|
|
272
|
-
return renderDetail(cmd, children, registry.prefix);
|
|
213
|
+
return path.length > 0 ? registry.detail(path, argv.session) : registry.overview(argv.session);
|
|
273
214
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
['
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
];
|
|
288
|
-
for (const [label, ok] of checks) {
|
|
289
|
-
lines.push(`- ${label}: ${ok ? '✅ 可用' : '❌ 不可用'}`);
|
|
290
|
-
}
|
|
291
|
-
const toolService = services.get('tools');
|
|
292
|
-
lines.push(`- 已注册工具: ${toolService ? toolService.getAll().length : 0} 个`);
|
|
293
|
-
lines.push(`- 已注册指令: ${registry.getAll().length} 个`);
|
|
294
|
-
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
|
+
]);
|
|
295
228
|
});
|
|
296
|
-
commands.command('shutdown', '
|
|
297
|
-
const host = app.
|
|
298
|
-
if (!host)
|
|
299
|
-
return '无法访问应用服务';
|
|
229
|
+
commands.use.command('shutdown', t('shutdownDescription'), { visibility: 'restricted' }).action(async () => {
|
|
230
|
+
const host = app.use;
|
|
300
231
|
setTimeout(async () => {
|
|
301
232
|
await host.stop();
|
|
302
233
|
process.exit(0);
|
|
303
234
|
}, 500);
|
|
304
|
-
return '
|
|
235
|
+
return t('shuttingDown');
|
|
305
236
|
});
|
|
306
|
-
commands.command('restart', '
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return '无法访问应用服务';
|
|
310
|
-
host.restart();
|
|
311
|
-
return '正在重启应用…';
|
|
237
|
+
commands.use.command('restart', t('restartDescription'), { visibility: 'restricted' }).action(async () => {
|
|
238
|
+
app.use.restart();
|
|
239
|
+
return t('restarting');
|
|
312
240
|
});
|
|
313
|
-
|
|
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) {
|
|
314
256
|
const isGlobal = scope === 'all';
|
|
315
257
|
// 预检:消息历史清不了就整条不执行。各插件的 memory:clear 中间件先于消息清理运行,放行就会清掉摘要、待办、
|
|
316
258
|
// 附件等,留下「消息还在、其余已清」的半截状态。预检只决定执行与否,清理时另取当时的记忆后端
|
|
317
259
|
const clearsMessages = !types || types.includes('context');
|
|
318
|
-
const memoryAtStart = memory.
|
|
260
|
+
const memoryAtStart = memory.present ? memory.use : undefined;
|
|
319
261
|
if (clearsMessages && !memoryAtStart)
|
|
320
|
-
return '
|
|
321
|
-
if (clearsMessages && isGlobal && !memoryAtStart?.clearAll)
|
|
322
|
-
return
|
|
323
|
-
}
|
|
262
|
+
return t('noMemoryBackend');
|
|
263
|
+
if (clearsMessages && isGlobal && !memoryAtStart?.clearAll)
|
|
264
|
+
return t('noGlobalClear');
|
|
324
265
|
const clearData = {
|
|
325
266
|
scope,
|
|
326
267
|
types,
|
|
327
268
|
sessionId: cmdCtx.sessionId,
|
|
328
269
|
results: [],
|
|
329
270
|
};
|
|
330
|
-
|
|
271
|
+
const run = () => hooks.use.run('memory:clear', clearData, async () => {
|
|
331
272
|
if (!clearsMessages)
|
|
332
273
|
return;
|
|
333
274
|
try {
|
|
334
275
|
// 中间件链可能要等一阵(在途落库、向量清空与保存),期间 memory 胜者可能已更换:按此刻的胜者清理,
|
|
335
276
|
// 不复用预检取到的实例,免得清到已下线的旧后端
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
277
|
+
if (!memory.present)
|
|
278
|
+
throw new TextError(t('memoryGone'));
|
|
279
|
+
const memoryService = memory.use;
|
|
339
280
|
if (isGlobal) {
|
|
340
281
|
if (!memoryService.clearAll)
|
|
341
|
-
throw new
|
|
282
|
+
throw new TextError(t('noGlobalClearMidway'));
|
|
342
283
|
await memoryService.clearAll();
|
|
343
284
|
clearData.results.push({
|
|
344
285
|
source: 'memory',
|
|
345
286
|
type: 'context',
|
|
346
287
|
success: true,
|
|
347
|
-
message: '
|
|
288
|
+
message: t('historyClearedAll'),
|
|
348
289
|
});
|
|
349
290
|
}
|
|
350
291
|
else {
|
|
@@ -353,80 +294,72 @@ function registerCommands({ commands, gateway, storage, memory, authority, app,
|
|
|
353
294
|
source: 'memory',
|
|
354
295
|
type: 'context',
|
|
355
296
|
success: true,
|
|
356
|
-
message: '
|
|
297
|
+
message: t('historyClearedSession'),
|
|
357
298
|
});
|
|
358
299
|
}
|
|
359
300
|
}
|
|
360
301
|
catch (err) {
|
|
361
|
-
|
|
362
|
-
|
|
302
|
+
clearData.results.push({
|
|
303
|
+
source: 'memory',
|
|
304
|
+
type: 'context',
|
|
305
|
+
success: false,
|
|
306
|
+
message: t('clearFailed', { error: textOf(err) }),
|
|
307
|
+
});
|
|
363
308
|
}
|
|
364
|
-
//
|
|
365
|
-
// 统一由各自的 memory:clear middleware 处理(见上面的附件缓存 middleware)。
|
|
309
|
+
// 附件、向量、人设、用户档案等子系统的清理由各自的 memory:clear 中间件处理(附件归写入它的插件)。
|
|
366
310
|
// runClear 仅负责调度 hook 与处理 memory 主体。
|
|
367
311
|
});
|
|
368
|
-
|
|
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);
|
|
369
318
|
// 显式要求的类型逐个核对:没有任何一行标注该类型,就是没有已启用的插件处理它,照实说明
|
|
370
|
-
for (const
|
|
371
|
-
if (!types?.includes(
|
|
319
|
+
for (const { id, label } of known) {
|
|
320
|
+
if (!types?.includes(id) || clearData.results.some(r => r.type === id))
|
|
372
321
|
continue;
|
|
373
|
-
lines.push(
|
|
374
|
-
? `⚠ ${t.label}只在 ${registry.prefix}clear all 时清理,本次未清理`
|
|
375
|
-
: `⚠ ${t.label}:没有已启用的插件处理这一类型,未清理`);
|
|
322
|
+
lines.push(t('unhandledType', { type: label }));
|
|
376
323
|
}
|
|
377
|
-
|
|
378
|
-
return '无可清除的记忆模块。';
|
|
379
|
-
return lines.join('\n');
|
|
324
|
+
return lines.length === 0 ? t('nothingCleared') : textLines(lines);
|
|
380
325
|
}
|
|
381
|
-
/**
|
|
382
|
-
* 清空共享会话(群/频道)所需的最低等级。私聊会话归用户本人,不受此限——
|
|
383
|
-
* 清自己的记忆是自助行为;群会话一人清掉会毁掉所有人的上下文,故要求同
|
|
384
|
-
* `visibility:'restricted'` 的等级 2。
|
|
385
|
-
*/
|
|
386
|
-
const CLEAR_SHARED_MIN_LEVEL = 2;
|
|
387
326
|
async function runClearFromOptions(argv, scope) {
|
|
327
|
+
const known = clearTypes();
|
|
388
328
|
let types;
|
|
389
329
|
try {
|
|
390
|
-
types = normalizeClearTypes(argv.options.type, registry.prefix);
|
|
330
|
+
types = normalizeClearTypes(argv.options.type, known, registry.prefix);
|
|
391
331
|
}
|
|
392
332
|
catch (err) {
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
// 共享会话设防:静态声明只能给出与会话归属无关的策略,这一档必须在运行期判。
|
|
396
|
-
if (argv.session.sessionType && argv.session.sessionType !== 'private') {
|
|
397
|
-
const auth = authority.current;
|
|
398
|
-
const isOwner = auth?.isOwner(argv.session.platform, argv.session.userId) ?? false;
|
|
399
|
-
const level = auth?.listUsers().find(u => u.platform === argv.session.platform && u.userId === argv.session.userId)?.level ??
|
|
400
|
-
0;
|
|
401
|
-
if (!isOwner && level < CLEAR_SHARED_MIN_LEVEL) {
|
|
402
|
-
return `清空共享会话需要等级 ${CLEAR_SHARED_MIN_LEVEL}(当前 ${level})。私聊里可以随时清理自己的会话。`;
|
|
403
|
-
}
|
|
333
|
+
return textOf(err);
|
|
404
334
|
}
|
|
405
|
-
return runClear({ sessionId: argv.session.sessionId }, scope, types);
|
|
335
|
+
return runClear({ sessionId: argv.session.sessionId }, scope, types, known);
|
|
406
336
|
}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
337
|
+
const clearTypeOption = {
|
|
338
|
+
description: t('clearTypeDescription'),
|
|
339
|
+
choices: () => [
|
|
340
|
+
{ value: 'all', description: t('clearTypeAll') },
|
|
341
|
+
...clearTypes().map(({ id, label }) => ({ value: id, description: label })),
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
commands.use
|
|
345
|
+
// 清空**当前会话**的记忆;确认挡住提示词注入触发的误清(需真人回一句)。
|
|
346
|
+
.command('clear', t('clearDescription'), {
|
|
347
|
+
risk: 'sensitive',
|
|
415
348
|
confirm: 'session',
|
|
416
349
|
})
|
|
417
|
-
.option('type', '-t <type:string[]>',
|
|
350
|
+
.option('type', '-t <type:string[]>', clearTypeOption)
|
|
418
351
|
.example('/clear')
|
|
419
352
|
.example('/clear --type context,vector')
|
|
420
353
|
.example('/clear -t vector -t image')
|
|
421
354
|
.example('/clear all --type all')
|
|
422
355
|
.action(async (argv) => runClearFromOptions(argv, 'session'));
|
|
423
|
-
commands
|
|
356
|
+
commands.use
|
|
424
357
|
// 全局清空,比 /clear 更重:同样 dangerous(等级 2 + 二次确认)。
|
|
425
358
|
// 原先只写 visibility:'restricted' 拿到了等级 2 但漏了 confirm。
|
|
426
|
-
.command('clear.all', '
|
|
359
|
+
.command('clear.all', t('clearAllDescription'), {
|
|
427
360
|
risk: 'dangerous',
|
|
428
361
|
})
|
|
429
|
-
.option('type', '-t <type:string[]>',
|
|
362
|
+
.option('type', '-t <type:string[]>', clearTypeOption)
|
|
430
363
|
.action(async (argv) => runClearFromOptions(argv, 'all'));
|
|
431
364
|
}
|
|
432
365
|
//# sourceMappingURL=index.js.map
|