dlinkclaw-openclaw 2026.10.26 → 2026.10.28
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/package.json +1 -1
- package/dist/src/approval-native.d.ts +11 -0
- package/dist/src/approval-native.js +22 -12
- package/dist/src/client/const.d.ts +3 -0
- package/dist/src/client/const.js +6 -0
- package/dist/src/client/dl-client.d.ts +14 -0
- package/dist/src/client/dl-client.js +31 -1
- package/dist/src/client/message.js +4 -0
- package/dist/src/services/index.js +16 -3
- package/dist/src/services/services.d.ts +17 -1
- package/dist/src/services/services.js +98 -2
- package/dist/src/services/types.d.ts +1 -1
- package/dist/src/stream-map.d.ts +6 -0
- package/dist/src/stream-map.js +32 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-10-29 dlink 原生审批投递能力。
|
|
3
|
+
* core 产生审批请求时交给插件,插件转发中继;发起人提示由客户端弹窗负责(不改写会话气泡)。
|
|
4
|
+
*/
|
|
5
|
+
type AnyObj = Record<string, any>;
|
|
1
6
|
export declare function createDLApprovalCapability(): any;
|
|
7
|
+
/**
|
|
8
|
+
* 2026-10-29 轮询路径入口:把审批推给中继。
|
|
9
|
+
* 发起人的等待审批提示由中继通知 + 客户端弹窗完成,这里不改写会话状态。
|
|
10
|
+
*/
|
|
11
|
+
export declare function announceWaiting(dlClient: any, payload: AnyObj): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -16,7 +16,7 @@ function pickRequest(ctx) {
|
|
|
16
16
|
return {
|
|
17
17
|
approvalId: firstString(payload.approvalId, req.id, req.approvalId),
|
|
18
18
|
kind: firstString(ctx?.approvalKind, req.kind, 'exec'),
|
|
19
|
-
agentId: firstString(req.agentId, view.agentId
|
|
19
|
+
agentId: firstString(req.agentId, view.agentId),
|
|
20
20
|
sessionKey: firstString(req.sessionKey, view.sessionKey),
|
|
21
21
|
command: firstString(payload.command, view.command, req.command, req.payload?.command),
|
|
22
22
|
cwd: firstString(payload.cwd, view.cwd, req.cwd),
|
|
@@ -25,18 +25,11 @@ function pickRequest(ctx) {
|
|
|
25
25
|
}
|
|
26
26
|
export function createDLApprovalCapability() {
|
|
27
27
|
return {
|
|
28
|
-
availability: {
|
|
29
|
-
// dlink 是唯一通道;账号已配置即可承接审批
|
|
30
|
-
isConfigured: () => true,
|
|
31
|
-
shouldHandle: () => true
|
|
32
|
-
},
|
|
28
|
+
availability: { isConfigured: () => true, shouldHandle: () => true },
|
|
33
29
|
presentation: {
|
|
34
30
|
buildPendingPayload: (ctx) => {
|
|
35
31
|
const info = pickRequest(ctx);
|
|
36
|
-
return {
|
|
37
|
-
...info,
|
|
38
|
-
text: firstString(ctx?.view?.summary, info.command)
|
|
39
|
-
};
|
|
32
|
+
return { ...info, text: firstString(ctx?.view?.summary, info.command) };
|
|
40
33
|
},
|
|
41
34
|
buildResolvedResult: () => ({ kind: 'leave' }),
|
|
42
35
|
buildExpiredResult: () => ({ kind: 'leave' })
|
|
@@ -65,10 +58,10 @@ export function createDLApprovalCapability() {
|
|
|
65
58
|
cwd: info.cwd,
|
|
66
59
|
expiresAtMs: info.expiresAtMs
|
|
67
60
|
});
|
|
68
|
-
logger.info?.(
|
|
61
|
+
logger.info?.('approval forwarded: id=' + info.approvalId + ' agent=' + info.agentId);
|
|
69
62
|
}
|
|
70
63
|
catch (e) {
|
|
71
|
-
logger.warn?.(
|
|
64
|
+
logger.warn?.('approval forward failed: ' + String(e));
|
|
72
65
|
}
|
|
73
66
|
return { approvalId: info.approvalId, deliveredAt: Date.now() };
|
|
74
67
|
},
|
|
@@ -78,3 +71,20 @@ export function createDLApprovalCapability() {
|
|
|
78
71
|
interactions: {}
|
|
79
72
|
};
|
|
80
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* 2026-10-29 轮询路径入口:把审批推给中继。
|
|
76
|
+
* 发起人的等待审批提示由中继通知 + 客户端弹窗完成,这里不改写会话状态。
|
|
77
|
+
*/
|
|
78
|
+
export async function announceWaiting(dlClient, payload) {
|
|
79
|
+
const logger = getDLLogger();
|
|
80
|
+
try {
|
|
81
|
+
const pushed = await dlClient.pushApprovalEvent(payload);
|
|
82
|
+
if (!pushed) {
|
|
83
|
+
await dlClient.publishEventToServer(payload, 'onApproval', '1');
|
|
84
|
+
}
|
|
85
|
+
logger.info?.('approval pushed: id=' + String(payload?.approvalId) + ' names=' + JSON.stringify(pushed?.approverNames ?? []));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
logger.warn?.('announceWaiting failed: ' + String(e));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -26,6 +26,9 @@ export declare const REPLY_SEND_TIMEOUT_MS = 15000;
|
|
|
26
26
|
export declare const AUTO_RELAY_API = "/guest/relayBot/sendNotification";
|
|
27
27
|
export declare const AUTO_RELAY_PERSONA = "/guest/relayBot/persona";
|
|
28
28
|
export declare const AUTO_RELAY_APPROVAL_EVENT = "/guest/relayBot/approvalEvent";
|
|
29
|
+
export declare const AUTO_RELAY_PERMISSION_CHECK = "/guest/relayBot/permissionCheck";
|
|
30
|
+
export declare const AUTO_RELAY_MODEL_CATALOG = "/guest/relayBot/modelCatalog";
|
|
31
|
+
export declare const AUTO_RELAY_MODEL_CHECK = "/guest/relayBot/modelCheck";
|
|
29
32
|
export declare const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
30
33
|
export declare const FILE_LIMITS: {
|
|
31
34
|
image: {
|
package/dist/src/client/const.js
CHANGED
|
@@ -27,6 +27,12 @@ export const AUTO_RELAY_API = '/guest/relayBot/sendNotification';
|
|
|
27
27
|
// 2026-10-26 人设注入:按 sessionKey 拉取 agent 人格与生效约定
|
|
28
28
|
export const AUTO_RELAY_PERSONA = '/guest/relayBot/persona';
|
|
29
29
|
export const AUTO_RELAY_APPROVAL_EVENT = '/guest/relayBot/approvalEvent';
|
|
30
|
+
// 2026-10-31 档位权限:插件在 apply 前回调中继校验
|
|
31
|
+
export const AUTO_RELAY_PERMISSION_CHECK = "/guest/relayBot/permissionCheck";
|
|
32
|
+
// 2026-10-31 模型目录上报
|
|
33
|
+
export const AUTO_RELAY_MODEL_CATALOG = "/guest/relayBot/modelCatalog";
|
|
34
|
+
// 2026-10-31 model authorization pre-check
|
|
35
|
+
export const AUTO_RELAY_MODEL_CHECK = "/guest/relayBot/modelCheck";
|
|
30
36
|
export const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
31
37
|
// ====== 上传文件配置常量(根据服务器性能调整)=======
|
|
32
38
|
export const FILE_LIMITS = {
|
|
@@ -53,6 +53,20 @@ export declare class DLClient extends EventEmitter {
|
|
|
53
53
|
* 失败返回 null,调用方降级为不注入,不影响正常对话。
|
|
54
54
|
*/
|
|
55
55
|
fetchPersona(sessionKey: string, agentId?: string): Promise<any | null>;
|
|
56
|
+
/**
|
|
57
|
+
* 2026-10-31 模型授权二次校验:应用模型覆盖前调用中继。
|
|
58
|
+
*/
|
|
59
|
+
checkModel(sessionKey: string, provider: string, model: string): Promise<any>;
|
|
60
|
+
/**
|
|
61
|
+
* 2026-10-31 模型目录上报:握手后把归一化的 provider/model 列表推给中继,
|
|
62
|
+
* 供客户端按规则过滤与切换校验使用。
|
|
63
|
+
*/
|
|
64
|
+
pushModelCatalog(payload: Record<string, unknown>): Promise<any>;
|
|
65
|
+
/**
|
|
66
|
+
* 2026-10-31 档位权限:应用 presetId 之前回调中继校验。
|
|
67
|
+
* 中继按 sessionKey 反查 agent 与操作人,插件不参与鉴权判定。
|
|
68
|
+
*/
|
|
69
|
+
checkPermission(sessionKey: string, presetId: string): Promise<any>;
|
|
56
70
|
/** 2026-10-27 把审批请求交给中继(由中继扇出给最多三人) */
|
|
57
71
|
pushApprovalEvent(payload: Record<string, unknown>): Promise<any>;
|
|
58
72
|
private fetchConfig;
|
|
@@ -3,7 +3,7 @@ import { buildDLReplyMessage, messagePreviewHandler } from './message.js';
|
|
|
3
3
|
import mqtt from 'mqtt';
|
|
4
4
|
import { DLHttp } from './dl-http.js';
|
|
5
5
|
import { decryptAesEcbPkcs5 } from './crypto.js';
|
|
6
|
-
import { AUTO_RELAY_API, AUTO_RELAY_PERSONA, AUTO_RELAY_APPROVAL_EVENT, AUTO_RELAY_UPLOAD } from './const.js';
|
|
6
|
+
import { AUTO_RELAY_API, AUTO_RELAY_PERSONA, AUTO_RELAY_APPROVAL_EVENT, AUTO_RELAY_UPLOAD, AUTO_RELAY_PERMISSION_CHECK, AUTO_RELAY_MODEL_CATALOG, AUTO_RELAY_MODEL_CHECK } from './const.js';
|
|
7
7
|
import { createCmdHandler } from '../services/index.js';
|
|
8
8
|
import { uuidStr } from '../utils.js';
|
|
9
9
|
export class DLClient extends EventEmitter {
|
|
@@ -66,6 +66,36 @@ export class DLClient extends EventEmitter {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* 2026-10-31 模型授权二次校验:应用模型覆盖前调用中继。
|
|
71
|
+
*/
|
|
72
|
+
async checkModel(sessionKey, provider, model) {
|
|
73
|
+
const data = await this.http.post(AUTO_RELAY_MODEL_CHECK, { sessionKey, provider, model });
|
|
74
|
+
return data ?? null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 2026-10-31 模型目录上报:握手后把归一化的 provider/model 列表推给中继,
|
|
78
|
+
* 供客户端按规则过滤与切换校验使用。
|
|
79
|
+
*/
|
|
80
|
+
async pushModelCatalog(payload) {
|
|
81
|
+
try {
|
|
82
|
+
// 2026-10-31 修正:中继 http 客户端签名会对参数做 trim,数组会报 value.trim is not a function;
|
|
83
|
+
// 这里统一序列化为字符串参数提交,由中继侧反序列化。
|
|
84
|
+
return await this.http.post(AUTO_RELAY_MODEL_CATALOG, { payload: JSON.stringify(payload) });
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
this.logger.warn('pushModelCatalog failed: ' + String(e));
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 2026-10-31 档位权限:应用 presetId 之前回调中继校验。
|
|
93
|
+
* 中继按 sessionKey 反查 agent 与操作人,插件不参与鉴权判定。
|
|
94
|
+
*/
|
|
95
|
+
async checkPermission(sessionKey, presetId) {
|
|
96
|
+
const data = await this.http.post(AUTO_RELAY_PERMISSION_CHECK, { sessionKey, presetId });
|
|
97
|
+
return data ?? null;
|
|
98
|
+
}
|
|
69
99
|
/** 2026-10-27 把审批请求交给中继(由中继扇出给最多三人) */
|
|
70
100
|
async pushApprovalEvent(payload) {
|
|
71
101
|
try {
|
|
@@ -4,6 +4,7 @@ import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
|
4
4
|
import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
5
5
|
import { beginPersonaScope, endPersonaScope } from "../persona.js";
|
|
6
6
|
import { uuidStr } from "../utils.js";
|
|
7
|
+
import { rememberStream, forgetStream } from "../stream-map.js";
|
|
7
8
|
/** 2026-10-18:只有包含复选框行(- [ ] / - [x] / - [/])的内容才算任务清单。 */
|
|
8
9
|
function isTaskListContent(content) {
|
|
9
10
|
return /(^|\n)\s*[-*]\s*\[[ xX/~%>]*\]\s*\S/.test(content);
|
|
@@ -993,6 +994,8 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
993
994
|
// 2026-10-26 群聊人设作用域:分发期间绑定被 @ 的 agent,整轮(含工具循环)都能读到正确值
|
|
994
995
|
// 2026-10-26 OpenClaw 的消息上下文字段名是 SessionKey(大写),兼容两种写法
|
|
995
996
|
personaScopeKey = ((ctx.SessionKey ?? ctx.sessionKey) || '');
|
|
997
|
+
// 2026-10-29 记录 sessionKey → streamId,审批等待提示需要
|
|
998
|
+
rememberStream(personaScopeKey, streamId, accountId);
|
|
996
999
|
logger.debug?.(`persona scope: session=${personaScopeKey} agent=${personaAgentId ?? '-'}`);
|
|
997
1000
|
if (personaAgentId) {
|
|
998
1001
|
beginPersonaScope(personaScopeKey, personaAgentId);
|
|
@@ -1039,6 +1042,7 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
1039
1042
|
},
|
|
1040
1043
|
onSettled: async () => {
|
|
1041
1044
|
endScopeOnce();
|
|
1045
|
+
forgetStream(personaScopeKey);
|
|
1042
1046
|
if ((accStatus.accumulatedPayload && accStatus.accumulatedPayload.length > 0) || mediaFiles.length > 0) {
|
|
1043
1047
|
//发送结束状态
|
|
1044
1048
|
await client.publishProcessingState({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CMD 消息处理器
|
|
3
3
|
* 集成到 duolian 的消息处理流程中
|
|
4
4
|
*/
|
|
5
|
+
import { announceWaiting } from '../approval-native.js';
|
|
5
6
|
import ClawServices from "./services.js";
|
|
6
7
|
const AGENT_RELAY_MAP = new Map();
|
|
7
8
|
// 2026-10-13 去重缓存(石头哥需求:更精准控制 + 减少不必要的 MQTT 推送):
|
|
@@ -25,9 +26,9 @@ export class CmdHandler {
|
|
|
25
26
|
// 2026-10-22:用量上报走服务端事件,由中继落库
|
|
26
27
|
void this.dlClient.publishEventToServer(payload, 'onUsage', '1');
|
|
27
28
|
}, (payload) => {
|
|
28
|
-
// 2026-10-
|
|
29
|
-
void this.dlClient
|
|
30
|
-
});
|
|
29
|
+
// 2026-10-29 审批推送:优先走 HTTP(可拿到审批人姓名),失败退回 MQTT 事件
|
|
30
|
+
void announceWaiting(this.dlClient, payload);
|
|
31
|
+
}, (sessionKey, presetId) => this.dlClient.checkPermission(sessionKey, presetId), (sessionKey, provider, model) => this.dlClient.checkModel(sessionKey, provider, model));
|
|
31
32
|
}
|
|
32
33
|
log(msg, type) {
|
|
33
34
|
this.services.log(msg, type);
|
|
@@ -49,6 +50,18 @@ export class CmdHandler {
|
|
|
49
50
|
});
|
|
50
51
|
this.services.on('onHandshaked', (payload) => {
|
|
51
52
|
this.dlClient.publishEventToServer(payload, 'onHandshaked', '0');
|
|
53
|
+
// 2026-10-31 模型目录上报:握手后推一份给中继(失败不影响连接)
|
|
54
|
+
void (async () => {
|
|
55
|
+
try {
|
|
56
|
+
const models = await this.services.exportModelCatalog();
|
|
57
|
+
if (models.length > 0) {
|
|
58
|
+
await this.dlClient.pushModelCatalog({ models });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// 上报失败不影响握手
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
52
65
|
});
|
|
53
66
|
this.services.on('onAgentReply', (payload) => {
|
|
54
67
|
const message = payload;
|
|
@@ -19,10 +19,16 @@ export declare class ClawServices extends EventEmitter {
|
|
|
19
19
|
/** 2026-10-22:token 用量上报(中继落库,按同事聚合) */
|
|
20
20
|
private readonly usagePublisher?;
|
|
21
21
|
private readonly approvalPublisher?;
|
|
22
|
+
/** 2026-10-31 档位权限:应用 preset 前回调中继校验(fail-closed) */
|
|
23
|
+
private readonly permissionChecker?;
|
|
24
|
+
/** 2026-10-31 模型授权:应用模型覆盖前回调中继校验 */
|
|
25
|
+
private readonly modelChecker?;
|
|
26
|
+
/** 2026-10-31 档位下调兜底:每会话最多 60s 检查一次,避免每条消息都多两次调用 */
|
|
27
|
+
private readonly permCeilingCheckedAt;
|
|
22
28
|
private approvalTimer;
|
|
23
29
|
private readonly approvalSeen;
|
|
24
30
|
private usageTimer;
|
|
25
|
-
constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink, usagePublisher?: (payload: Record<string, unknown>) => void, approvalPublisher?: (payload: Record<string, unknown>) => void);
|
|
31
|
+
constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink, usagePublisher?: (payload: Record<string, unknown>) => void, approvalPublisher?: (payload: Record<string, unknown>) => void, permissionChecker?: (sessionKey: string, presetId: string) => Promise<any>, modelChecker?: (sessionKey: string, provider: string, model: string) => Promise<any>);
|
|
26
32
|
/** 2026-10-27 每 10 秒本地查一次审批队列,出现新条目才推中继(推模式,非中继拉取) */
|
|
27
33
|
private startApprovalWatcher;
|
|
28
34
|
/** 2026-10-22:每 60 秒把最近一天的 token 用量上报中继(幂等 upsert) */
|
|
@@ -41,6 +47,12 @@ export declare class ClawServices extends EventEmitter {
|
|
|
41
47
|
*/
|
|
42
48
|
handleCmdMessage(cmdMessage: CmdMessage): Promise<CmdResponse>;
|
|
43
49
|
private handleAskAgent;
|
|
50
|
+
/**
|
|
51
|
+
* 2026-10-31 档位上限下调兜底。
|
|
52
|
+
* 只处理「会话显式设置过档位」的情况:读当前 permissionMode → 换回 presetId →
|
|
53
|
+
* 问中继是否仍允许;不允许则 patch 到 maxPresetId。任何异常都只记日志,不阻断。
|
|
54
|
+
*/
|
|
55
|
+
private enforcePermissionCeiling;
|
|
44
56
|
/**
|
|
45
57
|
* 更新
|
|
46
58
|
* @param agentId
|
|
@@ -83,6 +95,10 @@ export declare class ClawServices extends EventEmitter {
|
|
|
83
95
|
private handleStatusSystem;
|
|
84
96
|
private handleStatusUsage;
|
|
85
97
|
private handleListModels;
|
|
98
|
+
/**
|
|
99
|
+
* 2026-10-31 模型目录导出(给中继上报用):复用 models.list 归一化逻辑。
|
|
100
|
+
*/
|
|
101
|
+
exportModelCatalog(agentId?: string): Promise<Array<Record<string, unknown>>>;
|
|
86
102
|
private handleSessionList;
|
|
87
103
|
private handleSessionGet;
|
|
88
104
|
private handleSessionsDescribe;
|
|
@@ -35,15 +35,23 @@ export class ClawServices extends EventEmitter {
|
|
|
35
35
|
usagePublisher;
|
|
36
36
|
// 2026-10-27 审批事件推送(插件 → 中继),本地轮询发现新待审即上报
|
|
37
37
|
approvalPublisher;
|
|
38
|
+
/** 2026-10-31 档位权限:应用 preset 前回调中继校验(fail-closed) */
|
|
39
|
+
permissionChecker;
|
|
40
|
+
/** 2026-10-31 模型授权:应用模型覆盖前回调中继校验 */
|
|
41
|
+
modelChecker;
|
|
42
|
+
/** 2026-10-31 档位下调兜底:每会话最多 60s 检查一次,避免每条消息都多两次调用 */
|
|
43
|
+
permCeilingCheckedAt = new Map();
|
|
38
44
|
approvalTimer = null;
|
|
39
45
|
approvalSeen = new Set();
|
|
40
46
|
usageTimer = null;
|
|
41
|
-
constructor(options, logger, terminalSink, usagePublisher, approvalPublisher) {
|
|
47
|
+
constructor(options, logger, terminalSink, usagePublisher, approvalPublisher, permissionChecker, modelChecker) {
|
|
42
48
|
super();
|
|
43
49
|
this.options = options;
|
|
44
50
|
this.logger = logger;
|
|
45
51
|
this.usagePublisher = usagePublisher;
|
|
46
52
|
this.approvalPublisher = approvalPublisher;
|
|
53
|
+
this.permissionChecker = permissionChecker;
|
|
54
|
+
this.modelChecker = modelChecker;
|
|
47
55
|
this.terminalManager = new TerminalManager(terminalSink ?? (() => { }), (msg, type) => this.log(msg, type ?? 'info'));
|
|
48
56
|
}
|
|
49
57
|
/** 2026-10-27 每 10 秒本地查一次审批队列,出现新条目才推中继(推模式,非中继拉取) */
|
|
@@ -70,7 +78,7 @@ export class ClawServices extends EventEmitter {
|
|
|
70
78
|
continue;
|
|
71
79
|
}
|
|
72
80
|
this.approvalSeen.add(String(id));
|
|
73
|
-
publish({
|
|
81
|
+
const pushed = await publish({
|
|
74
82
|
approvalId: String(id),
|
|
75
83
|
kind,
|
|
76
84
|
agentId: item?.agentId ?? item?.request?.agentId ?? '',
|
|
@@ -80,6 +88,7 @@ export class ClawServices extends EventEmitter {
|
|
|
80
88
|
createdAtMs: item?.createdAtMs ?? 0,
|
|
81
89
|
expiresAtMs: item?.expiresAtMs ?? 0
|
|
82
90
|
});
|
|
91
|
+
void pushed;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
if (this.approvalSeen.size > 500) {
|
|
@@ -284,6 +293,10 @@ export class ClawServices extends EventEmitter {
|
|
|
284
293
|
result = await this.handleStatusUsage();
|
|
285
294
|
break;
|
|
286
295
|
// 模型管理
|
|
296
|
+
case 'models.catalog':
|
|
297
|
+
// 2026-10-31 模型目录(给中继按需拉取):包装成对象,避免 data 为数组导致中继反序列化失败
|
|
298
|
+
result = { models: await this.handleListModels(cmdMessage.params) };
|
|
299
|
+
break;
|
|
287
300
|
case 'models.list':
|
|
288
301
|
// 2026-09-06 适配 openclaw 9.2:models.list 需显式 agentId(多 agent 配置下无 owner 会报 INVALID_REQUEST)
|
|
289
302
|
result = await this.handleListModels(cmdMessage.params);
|
|
@@ -453,8 +466,52 @@ export class ClawServices extends EventEmitter {
|
|
|
453
466
|
}
|
|
454
467
|
}
|
|
455
468
|
async handleAskAgent(params) {
|
|
469
|
+
// 2026-10-31 方案A:管理员调低上限后,已有会话在下次提问前自动降级(失败不影响发消息)
|
|
470
|
+
await this.enforcePermissionCeiling(params?.sessionKey);
|
|
456
471
|
return await this.client?.askAgent(params);
|
|
457
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* 2026-10-31 档位上限下调兜底。
|
|
475
|
+
* 只处理「会话显式设置过档位」的情况:读当前 permissionMode → 换回 presetId →
|
|
476
|
+
* 问中继是否仍允许;不允许则 patch 到 maxPresetId。任何异常都只记日志,不阻断。
|
|
477
|
+
*/
|
|
478
|
+
async enforcePermissionCeiling(sessionKey) {
|
|
479
|
+
if (!sessionKey || !this.permissionChecker) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
const last = this.permCeilingCheckedAt.get(sessionKey) ?? 0;
|
|
484
|
+
if (now - last < 60_000) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.permCeilingCheckedAt.set(sessionKey, now);
|
|
488
|
+
try {
|
|
489
|
+
const row = await this.client?.sessionsDescribe({ key: sessionKey });
|
|
490
|
+
const mode = row?.permissionMode ?? row?.session?.permissionMode ?? null;
|
|
491
|
+
if (!mode) {
|
|
492
|
+
return; // 继承态,没有显式档位
|
|
493
|
+
}
|
|
494
|
+
const presetId = Object.entries(ClawServices.PERMISSION_PRESETS).find(([, v]) => v === mode)?.[0];
|
|
495
|
+
if (!presetId || presetId === 'default') {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const chk = await this.permissionChecker(sessionKey, presetId);
|
|
499
|
+
const payload = chk?.data ?? chk;
|
|
500
|
+
if (!payload || payload.allowed !== false) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const maxPresetId = String(payload.maxPresetId ?? 'guarded');
|
|
504
|
+
const maxMode = ClawServices.PERMISSION_PRESETS[maxPresetId];
|
|
505
|
+
if (!maxMode) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
await this.client?.sessionsPatch({ key: sessionKey, patch: { permissionMode: maxMode } });
|
|
509
|
+
this.log('permission ceiling enforced: session=' + sessionKey + ' ' + presetId + ' -> ' + maxPresetId, 'warn');
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
this.log('enforcePermissionCeiling failed, ignore: ' + String(e), 'warn');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
458
515
|
/**
|
|
459
516
|
* 更新
|
|
460
517
|
* @param agentId
|
|
@@ -469,6 +526,23 @@ export class ClawServices extends EventEmitter {
|
|
|
469
526
|
if ((!provider || provider.length === 0 || !model || model.length === 0) && !thinkingLevel) {
|
|
470
527
|
return null;
|
|
471
528
|
}
|
|
529
|
+
// 2026-10-31 模型授权:应用覆盖前回调中继校验。
|
|
530
|
+
// 明确 denied 才拒绝;网络异常放行,避免中继短暂不可用时把人锁死。
|
|
531
|
+
if (provider && model && this.modelChecker) {
|
|
532
|
+
try {
|
|
533
|
+
const chk = await this.modelChecker(sessionKey, provider, model);
|
|
534
|
+
const payload = chk?.data ?? chk;
|
|
535
|
+
if (payload && payload.allowed === false) {
|
|
536
|
+
throw new Error('model not authorized: ' + provider + '/' + model);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
if (String(e).includes('model not authorized')) {
|
|
541
|
+
throw e;
|
|
542
|
+
}
|
|
543
|
+
this.log('modelCheck failed, allow: ' + String(e), 'warn');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
472
546
|
// 推理强度合法值校验(off/minimal/low/medium/high),非法值直接忽略
|
|
473
547
|
const THINK_LEVELS = ['off', 'minimal', 'low', 'medium', 'high'];
|
|
474
548
|
const thinkOverride = thinkingLevel && THINK_LEVELS.includes(thinkingLevel)
|
|
@@ -894,6 +968,19 @@ export class ClawServices extends EventEmitter {
|
|
|
894
968
|
});
|
|
895
969
|
}
|
|
896
970
|
//#endregion
|
|
971
|
+
/**
|
|
972
|
+
* 2026-10-31 模型目录导出(给中继上报用):复用 models.list 归一化逻辑。
|
|
973
|
+
*/
|
|
974
|
+
async exportModelCatalog(agentId) {
|
|
975
|
+
try {
|
|
976
|
+
const res = await this.handleListModels(agentId ? { agentId } : undefined);
|
|
977
|
+
return Array.isArray(res) ? res : [];
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
this.log('exportModelCatalog failed: ' + String(e), 'warn');
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
897
984
|
//#region ===================== 会话处理器 ======================
|
|
898
985
|
async handleSessionList(params) {
|
|
899
986
|
return await this.client?.sessionsList(params);
|
|
@@ -1096,6 +1183,15 @@ export class ClawServices extends EventEmitter {
|
|
|
1096
1183
|
if (!(presetId in ClawServices.PERMISSION_PRESETS)) {
|
|
1097
1184
|
throw new Error('未知档位: ' + presetId);
|
|
1098
1185
|
}
|
|
1186
|
+
// 2026-10-31 档位权限:以中继判定为准,拒绝即中止(fail-closed)
|
|
1187
|
+
if (this.permissionChecker) {
|
|
1188
|
+
const chk = await this.permissionChecker(sessionKey, presetId);
|
|
1189
|
+
const payload = chk?.data ?? chk;
|
|
1190
|
+
if (payload?.allowed !== true) {
|
|
1191
|
+
const reason = payload?.reason ?? chk?.error ?? 'permission denied';
|
|
1192
|
+
throw new Error(`permissions.set denied: ${reason}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1099
1195
|
const mode = ClawServices.PERMISSION_PRESETS[presetId];
|
|
1100
1196
|
// 2026-10-27 写入后校验:网关可能返回 success=false 却不抛异常,也可能接受了但没落库
|
|
1101
1197
|
const res = await this.client?.sessionsPatch({ key: sessionKey, patch: { permissionMode: mode } });
|
|
@@ -306,7 +306,7 @@ export interface CmdResult {
|
|
|
306
306
|
timestamp: string | number;
|
|
307
307
|
timezone: string;
|
|
308
308
|
}
|
|
309
|
-
export type CmdType = 'agent.ask' | 'agent.list' | 'agent.create' | 'agent.update' | 'agent.delete' | 'agent.get' | 'agent.stop' | 'chat.abort' | 'agent.capabilities' | 'permissions.get' | 'permissions.set' | 'approvals.pending' | 'approvals.resolve' | 'task.list' | 'task.create' | 'task.update' | 'task.delete' | 'task.run' | 'status.health' | 'status.system' | 'status.usage' | 'models.list' | 'sessions.list' | 'sessions.get' | 'sessions.describe' | 'sessions.resolve' | 'sessions.create' | 'sessions.archive' | 'sessions.restore' | 'sessions.delete' | 'sessions.reset' | 'sessions.patch' | 'sessions.currentModel' | 'sessions.abort' | 'terminal.open' | 'terminal.input' | 'terminal.resize' | 'terminal.close' | 'chat.history' | 'chat.trajectory' | 'skills.status' | 'skills.search' | 'skills.detail' | 'skills.install' | 'skills.update' | 'skills.securityVerdicts' | 'cancelMessage' | 'usage.cost' | 'sessions.usage' | 'sessions.usage.logs' | 'pairing.approve' | 'artifacts.list' | 'artifacts.get' | 'artifacts.download' | 'agents.files.list' | 'agents.files.get' | 'extCapabilities.status' | 'extCapabilities.check' | 'extCapabilities.install';
|
|
309
|
+
export type CmdType = 'agent.ask' | 'agent.list' | 'agent.create' | 'agent.update' | 'agent.delete' | 'agent.get' | 'agent.stop' | 'chat.abort' | 'agent.capabilities' | 'permissions.get' | 'permissions.set' | 'approvals.pending' | 'approvals.resolve' | 'task.list' | 'task.create' | 'task.update' | 'task.delete' | 'task.run' | 'status.health' | 'status.system' | 'status.usage' | 'models.list' | 'models.catalog' | 'sessions.list' | 'sessions.get' | 'sessions.describe' | 'sessions.resolve' | 'sessions.create' | 'sessions.archive' | 'sessions.restore' | 'sessions.delete' | 'sessions.reset' | 'sessions.patch' | 'sessions.currentModel' | 'sessions.abort' | 'terminal.open' | 'terminal.input' | 'terminal.resize' | 'terminal.close' | 'chat.history' | 'chat.trajectory' | 'skills.status' | 'skills.search' | 'skills.detail' | 'skills.install' | 'skills.update' | 'skills.securityVerdicts' | 'cancelMessage' | 'usage.cost' | 'sessions.usage' | 'sessions.usage.logs' | 'pairing.approve' | 'artifacts.list' | 'artifacts.get' | 'artifacts.download' | 'agents.files.list' | 'agents.files.get' | 'extCapabilities.status' | 'extCapabilities.check' | 'extCapabilities.install';
|
|
310
310
|
export type ReadlineType = Extract<CmdType, 'chat.trajectory'>;
|
|
311
311
|
export interface ClawMessage {
|
|
312
312
|
type: 'res' | 'event';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function rememberStream(sessionKey: string, streamId: string, accountId: string): void;
|
|
2
|
+
export declare function streamOf(sessionKey: string): {
|
|
3
|
+
streamId: string;
|
|
4
|
+
accountId: string;
|
|
5
|
+
} | undefined;
|
|
6
|
+
export declare function forgetStream(sessionKey: string): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-10-29 sessionKey → 当前运行 streamId 映射。
|
|
3
|
+
* 审批发生时只有 sessionKey,而推送处理状态需要 streamId。
|
|
4
|
+
*/
|
|
5
|
+
const map = new Map();
|
|
6
|
+
const TTL_MS = 30 * 60 * 1000;
|
|
7
|
+
export function rememberStream(sessionKey, streamId, accountId) {
|
|
8
|
+
if (!sessionKey || !streamId) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// 2026-10-29 sessionKey 大小写可能不一致,统一小写归一
|
|
12
|
+
map.set(sessionKey.toLowerCase(), { streamId, accountId, at: Date.now() });
|
|
13
|
+
}
|
|
14
|
+
export function streamOf(sessionKey) {
|
|
15
|
+
if (!sessionKey) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const hit = map.get(sessionKey.toLowerCase());
|
|
19
|
+
if (!hit) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (Date.now() - hit.at > TTL_MS) {
|
|
23
|
+
map.delete(sessionKey.toLowerCase());
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return { streamId: hit.streamId, accountId: hit.accountId };
|
|
27
|
+
}
|
|
28
|
+
export function forgetStream(sessionKey) {
|
|
29
|
+
if (sessionKey) {
|
|
30
|
+
map.delete(sessionKey.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
}
|