feat(goal): TG /goal 指令进入目标模式
- 复用宿主命令运行时(commands.execute,与 Web 端 /goal 同通道),不依赖 GUI 连接:command-goal 在宿主侧操作 session-log 支撑的 goals 域 - 语法与 Web 完全一致:/goal <目标>、/goal(查看)、clear/edit/pause/resume - 目标状态变更(blocked/complete)实时推送 TG 通知,手机可感知目标模式进展 - 配置侧启用 command-goal(plugin_toggle,已 live 生效)
This commit is contained in:
Vendored
+5
@@ -85,6 +85,11 @@ export declare class TelegramBridge {
|
|||||||
/** 按名称解析宿主服务(Cordis 需 ctx.get;mock/plain ctx 走自有属性兜底)。 */
|
/** 按名称解析宿主服务(Cordis 需 ctx.get;mock/plain ctx 走自有属性兜底)。 */
|
||||||
private serviceOf;
|
private serviceOf;
|
||||||
private requestCompact;
|
private requestCompact;
|
||||||
|
/**
|
||||||
|
* TG /goal —— 走宿主命令运行时(commands.execute),与 Web 端 /goal 完全同一条
|
||||||
|
* 通道:command-goal 在宿主侧操作 session-log 支撑的 goals 域,不依赖 GUI 连接。
|
||||||
|
*/
|
||||||
|
private requestGoal;
|
||||||
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
||||||
private runCompaction;
|
private runCompaction;
|
||||||
private followupBound;
|
private followupBound;
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export class TelegramBridge {
|
|||||||
{ command: 'model', description: '切换当前绑定会话的模型' },
|
{ command: 'model', description: '切换当前绑定会话的模型' },
|
||||||
{ command: 'status', description: '会话状态:模型/上下文/统计(首token/速率/tokens)' },
|
{ command: 'status', description: '会话状态:模型/上下文/统计(首token/速率/tokens)' },
|
||||||
{ command: 'compact', description: '压缩当前会话历史(缩短上下文,空闲时执行)' },
|
{ command: 'compact', description: '压缩当前会话历史(缩短上下文,空闲时执行)' },
|
||||||
|
{ command: 'goal', description: '目标模式:/goal <目标>(clear/edit/pause/resume 管理)' },
|
||||||
{ command: 'rich', description: '渲染:on 富文本 / off HTML 兼容(本聊天,需新版客户端)' },
|
{ command: 'rich', description: '渲染:on 富文本 / off HTML 兼容(本聊天,需新版客户端)' },
|
||||||
{ command: 'unbind', description: '断开手机绑定(不关闭本机会话)' },
|
{ command: 'unbind', description: '断开手机绑定(不关闭本机会话)' },
|
||||||
{ command: 'stop', description: '中止当前正在运行的任务' },
|
{ command: 'stop', description: '中止当前正在运行的任务' },
|
||||||
@@ -309,6 +310,9 @@ export class TelegramBridge {
|
|||||||
case 'compact':
|
case 'compact':
|
||||||
await this.requestCompact(chatId);
|
await this.requestCompact(chatId);
|
||||||
return;
|
return;
|
||||||
|
case 'goal':
|
||||||
|
await this.requestGoal(chatId, parsed.arg);
|
||||||
|
return;
|
||||||
case 'rich':
|
case 'rich':
|
||||||
await this.handleRichCommand(chatId, parsed.arg);
|
await this.handleRichCommand(chatId, parsed.arg);
|
||||||
return;
|
return;
|
||||||
@@ -895,6 +899,41 @@ export class TelegramBridge {
|
|||||||
this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`);
|
this.ctx.logger.error(`dsh-telegram-channel: /compact runner crashed: ${this.redact(err)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* TG /goal —— 走宿主命令运行时(commands.execute),与 Web 端 /goal 完全同一条
|
||||||
|
* 通道:command-goal 在宿主侧操作 session-log 支撑的 goals 域,不依赖 GUI 连接。
|
||||||
|
*/
|
||||||
|
async requestGoal(chatId, arg) {
|
||||||
|
const binding = this.bindings.get(String(chatId));
|
||||||
|
if (!binding) {
|
||||||
|
await this.client.sendMessage(chatId, MSG.NEED_BIND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const agent = await this.ensureLiveAgent(binding.sessionId);
|
||||||
|
if (!agent) {
|
||||||
|
this.bindings.delete(String(chatId));
|
||||||
|
await this.client.sendMessage(chatId, MSG.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const commands = this.serviceOf('commands');
|
||||||
|
if (!commands?.execute || typeof commands.find !== 'function' || !commands.find(agent, 'goal')) {
|
||||||
|
await this.client.sendMessage(chatId, '当前环境不支持 /goal(command-goal 未装配)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const line = `/goal${arg ? ` ${arg}` : ''}`;
|
||||||
|
try {
|
||||||
|
const execution = await commands.execute(agent, line, [], new AbortController().signal);
|
||||||
|
if (execution === undefined) {
|
||||||
|
await this.client.sendMessage(chatId, '/goal 未执行:命令在运行时已注销');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = execution.result?.text ?? (execution.result?.kind === 'success' ? '完成' : '失败');
|
||||||
|
await this.client.sendMessage(chatId, text.slice(0, 3500));
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
await this.client.sendMessage(chatId, `/goal 执行失败:${this.redact(err).slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
||||||
async runCompaction(chatId, sessionId, operation, controller) {
|
async runCompaction(chatId, sessionId, operation, controller) {
|
||||||
try {
|
try {
|
||||||
@@ -1204,6 +1243,23 @@ export class TelegramBridge {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 'goal/changed' 不在本插件的 SessionEvent 类型联合内,宽松断言读取。
|
||||||
|
const eventType = event.type;
|
||||||
|
if (eventType === 'goal/changed') {
|
||||||
|
// Goal mode lives on the phone too: surface phase transitions (blocked/complete
|
||||||
|
// especially) without waiting for the next assistant text.
|
||||||
|
const data = event.data;
|
||||||
|
const phase = data?.phase;
|
||||||
|
if (phase === 'blocked' || phase === 'complete') {
|
||||||
|
const reason = data?.blockedReason;
|
||||||
|
const label = phase === 'blocked'
|
||||||
|
? `目标被阻塞${reason ? `:${reason.code ?? ''} ${reason.message ?? ''}`.slice(0, 120) : ''}`
|
||||||
|
: '目标已达成 ✓';
|
||||||
|
for (const b of targets)
|
||||||
|
this.enqueueNotice(b.chatId, `> ${label}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.type === 'todo/write') {
|
if (event.type === 'todo/write') {
|
||||||
const todos = Array.isArray(event.data?.todos) ? event.data.todos : [];
|
const todos = Array.isArray(event.data?.todos) ? event.data.todos : [];
|
||||||
this.lastTodos.set(id, todos);
|
this.lastTodos.set(id, todos);
|
||||||
|
|||||||
Vendored
+4
@@ -67,6 +67,10 @@ export type ParsedCommand = {
|
|||||||
type: 'rich';
|
type: 'rich';
|
||||||
text: string;
|
text: string;
|
||||||
arg?: string;
|
arg?: string;
|
||||||
|
} | {
|
||||||
|
type: 'goal';
|
||||||
|
text: string;
|
||||||
|
arg?: string;
|
||||||
} | {
|
} | {
|
||||||
type: 'unbind';
|
type: 'unbind';
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ export function parseCommand(text) {
|
|||||||
return { type: 'status', text };
|
return { type: 'status', text };
|
||||||
case '/compact':
|
case '/compact':
|
||||||
return { type: 'compact', text };
|
return { type: 'compact', text };
|
||||||
|
case '/goal':
|
||||||
|
case '/目标':
|
||||||
|
// 目标语句是多词文本,保留完整参数(去掉命令名本身)
|
||||||
|
return { type: 'goal', text, arg: text.slice(command.length).trim() };
|
||||||
case '/rich':
|
case '/rich':
|
||||||
case '/render':
|
case '/render':
|
||||||
case '/setting': {
|
case '/setting': {
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ export class TelegramBridge {
|
|||||||
{ command: 'model', description: '切换当前绑定会话的模型' },
|
{ command: 'model', description: '切换当前绑定会话的模型' },
|
||||||
{ command: 'status', description: '会话状态:模型/上下文/统计(首token/速率/tokens)' },
|
{ command: 'status', description: '会话状态:模型/上下文/统计(首token/速率/tokens)' },
|
||||||
{ command: 'compact', description: '压缩当前会话历史(缩短上下文,空闲时执行)' },
|
{ command: 'compact', description: '压缩当前会话历史(缩短上下文,空闲时执行)' },
|
||||||
|
{ command: 'goal', description: '目标模式:/goal <目标>(clear/edit/pause/resume 管理)' },
|
||||||
{ command: 'rich', description: '渲染:on 富文本 / off HTML 兼容(本聊天,需新版客户端)' },
|
{ command: 'rich', description: '渲染:on 富文本 / off HTML 兼容(本聊天,需新版客户端)' },
|
||||||
{ command: 'unbind', description: '断开手机绑定(不关闭本机会话)' },
|
{ command: 'unbind', description: '断开手机绑定(不关闭本机会话)' },
|
||||||
{ command: 'stop', description: '中止当前正在运行的任务' },
|
{ command: 'stop', description: '中止当前正在运行的任务' },
|
||||||
@@ -451,6 +452,9 @@ export class TelegramBridge {
|
|||||||
case 'compact':
|
case 'compact':
|
||||||
await this.requestCompact(chatId)
|
await this.requestCompact(chatId)
|
||||||
return
|
return
|
||||||
|
case 'goal':
|
||||||
|
await this.requestGoal(chatId, (parsed as { arg?: string }).arg)
|
||||||
|
return
|
||||||
case 'rich':
|
case 'rich':
|
||||||
await this.handleRichCommand(chatId, parsed.arg)
|
await this.handleRichCommand(chatId, parsed.arg)
|
||||||
return
|
return
|
||||||
@@ -1050,6 +1054,41 @@ export class TelegramBridge {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TG /goal —— 走宿主命令运行时(commands.execute),与 Web 端 /goal 完全同一条
|
||||||
|
* 通道:command-goal 在宿主侧操作 session-log 支撑的 goals 域,不依赖 GUI 连接。
|
||||||
|
*/
|
||||||
|
private async requestGoal(chatId: number, arg?: string): Promise<void> {
|
||||||
|
const binding = this.bindings.get(String(chatId))
|
||||||
|
if (!binding) {
|
||||||
|
await this.client.sendMessage(chatId, MSG.NEED_BIND)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const agent = await this.ensureLiveAgent(binding.sessionId)
|
||||||
|
if (!agent) {
|
||||||
|
this.bindings.delete(String(chatId))
|
||||||
|
await this.client.sendMessage(chatId, MSG.GONE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const commands = this.serviceOf<CommandsRuntimeLike>('commands')
|
||||||
|
if (!commands?.execute || typeof commands.find !== 'function' || !commands.find(agent, 'goal')) {
|
||||||
|
await this.client.sendMessage(chatId, '当前环境不支持 /goal(command-goal 未装配)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const line = `/goal${arg ? ` ${arg}` : ''}`
|
||||||
|
try {
|
||||||
|
const execution = await commands.execute(agent as never, line, [], new AbortController().signal)
|
||||||
|
if (execution === undefined) {
|
||||||
|
await this.client.sendMessage(chatId, '/goal 未执行:命令在运行时已注销')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const text = execution.result?.text ?? (execution.result?.kind === 'success' ? '完成' : '失败')
|
||||||
|
await this.client.sendMessage(chatId, text.slice(0, 3500))
|
||||||
|
} catch (err) {
|
||||||
|
await this.client.sendMessage(chatId, `/goal 执行失败:${this.redact(err).slice(0, 200)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
/** 等待压缩落定并汇报结果(不阻塞轮询循环)。 */
|
||||||
private async runCompaction(
|
private async runCompaction(
|
||||||
chatId: number,
|
chatId: number,
|
||||||
@@ -1367,6 +1406,23 @@ export class TelegramBridge {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 'goal/changed' 不在本插件的 SessionEvent 类型联合内,宽松断言读取。
|
||||||
|
const eventType = (event as { type: string }).type
|
||||||
|
if (eventType === 'goal/changed') {
|
||||||
|
// Goal mode lives on the phone too: surface phase transitions (blocked/complete
|
||||||
|
// especially) without waiting for the next assistant text.
|
||||||
|
const data = (event as { data?: { phase?: string; blockedReason?: { code?: string; message?: string } } }).data
|
||||||
|
const phase = data?.phase
|
||||||
|
if (phase === 'blocked' || phase === 'complete') {
|
||||||
|
const reason = data?.blockedReason
|
||||||
|
const label = phase === 'blocked'
|
||||||
|
? `目标被阻塞${reason ? `:${reason.code ?? ''} ${reason.message ?? ''}`.slice(0, 120) : ''}`
|
||||||
|
: '目标已达成 ✓'
|
||||||
|
for (const b of targets) this.enqueueNotice(b.chatId, `> ${label}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'todo/write') {
|
if (event.type === 'todo/write') {
|
||||||
const todos = Array.isArray(event.data?.todos) ? event.data.todos : []
|
const todos = Array.isArray(event.data?.todos) ? event.data.todos : []
|
||||||
this.lastTodos.set(id, todos as Array<{ content?: string; status?: string }>)
|
this.lastTodos.set(id, todos as Array<{ content?: string; status?: string }>)
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export type ParsedCommand =
|
|||||||
| { type: 'status'; text: string }
|
| { type: 'status'; text: string }
|
||||||
| { type: 'compact'; text: string }
|
| { type: 'compact'; text: string }
|
||||||
| { type: 'rich'; text: string; arg?: string }
|
| { type: 'rich'; text: string; arg?: string }
|
||||||
|
| { type: 'goal'; text: string; arg?: string }
|
||||||
| { type: 'unbind'; text: string }
|
| { type: 'unbind'; text: string }
|
||||||
| { type: 'stop'; text: string }
|
| { type: 'stop'; text: string }
|
||||||
| { type: 'mission'; text: string }
|
| { type: 'mission'; text: string }
|
||||||
@@ -140,6 +141,10 @@ export function parseCommand(text: string): ParsedCommand {
|
|||||||
return { type: 'status', text }
|
return { type: 'status', text }
|
||||||
case '/compact':
|
case '/compact':
|
||||||
return { type: 'compact', text }
|
return { type: 'compact', text }
|
||||||
|
case '/goal':
|
||||||
|
case '/目标':
|
||||||
|
// 目标语句是多词文本,保留完整参数(去掉命令名本身)
|
||||||
|
return { type: 'goal', text, arg: text.slice(command.length).trim() }
|
||||||
case '/rich':
|
case '/rich':
|
||||||
case '/render':
|
case '/render':
|
||||||
case '/setting': {
|
case '/setting': {
|
||||||
|
|||||||
Reference in New Issue
Block a user