dlinkclaw-openclaw 2026.10.25 → 2026.10.27
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 +12 -0
- package/dist/src/approval-native.js +90 -0
- package/dist/src/channel.js +3 -0
- package/dist/src/client/const.d.ts +1 -0
- package/dist/src/client/const.js +1 -0
- package/dist/src/client/dl-client.d.ts +2 -0
- package/dist/src/client/dl-client.js +12 -1
- package/dist/src/client/message.js +30 -13
- package/dist/src/exec-policy.d.ts +6 -0
- package/dist/src/exec-policy.js +39 -0
- package/dist/src/services/index.js +4 -0
- package/dist/src/services/services.d.ts +16 -1
- package/dist/src/services/services.js +201 -5
- package/dist/src/services/types.d.ts +2 -2
- package/dist/src/services/ws-client.d.ts +14 -1
- package/dist/src/services/ws-client.js +25 -2
- 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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-10-29 dlink 原生审批投递能力。
|
|
3
|
+
* core 产生审批请求时交给插件,插件转发中继;发起人提示由客户端弹窗负责(不改写会话气泡)。
|
|
4
|
+
*/
|
|
5
|
+
type AnyObj = Record<string, any>;
|
|
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 {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
import { getDLClient } from './client/client-manager.js';
|
|
3
|
+
import { getDLLogger } from './runtime.js';
|
|
4
|
+
function firstString(...vals) {
|
|
5
|
+
for (const v of vals) {
|
|
6
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
7
|
+
return v;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
function pickRequest(ctx) {
|
|
13
|
+
const req = ctx?.request ?? {};
|
|
14
|
+
const view = ctx?.view ?? {};
|
|
15
|
+
const payload = ctx?.pendingPayload ?? {};
|
|
16
|
+
return {
|
|
17
|
+
approvalId: firstString(payload.approvalId, req.id, req.approvalId),
|
|
18
|
+
kind: firstString(ctx?.approvalKind, req.kind, 'exec'),
|
|
19
|
+
agentId: firstString(req.agentId, view.agentId),
|
|
20
|
+
sessionKey: firstString(req.sessionKey, view.sessionKey),
|
|
21
|
+
command: firstString(payload.command, view.command, req.command, req.payload?.command),
|
|
22
|
+
cwd: firstString(payload.cwd, view.cwd, req.cwd),
|
|
23
|
+
expiresAtMs: Number(payload.expiresAtMs ?? view.expiresAtMs ?? req.expiresAtMs ?? 0)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function createDLApprovalCapability() {
|
|
27
|
+
return {
|
|
28
|
+
availability: { isConfigured: () => true, shouldHandle: () => true },
|
|
29
|
+
presentation: {
|
|
30
|
+
buildPendingPayload: (ctx) => {
|
|
31
|
+
const info = pickRequest(ctx);
|
|
32
|
+
return { ...info, text: firstString(ctx?.view?.summary, info.command) };
|
|
33
|
+
},
|
|
34
|
+
buildResolvedResult: () => ({ kind: 'leave' }),
|
|
35
|
+
buildExpiredResult: () => ({ kind: 'leave' })
|
|
36
|
+
},
|
|
37
|
+
transport: {
|
|
38
|
+
prepareTarget: (ctx) => ctx?.plannedTarget ?? null,
|
|
39
|
+
deliverPending: async (ctx) => {
|
|
40
|
+
const logger = getDLLogger();
|
|
41
|
+
const info = pickRequest(ctx);
|
|
42
|
+
if (!info.approvalId) {
|
|
43
|
+
logger.warn?.('approval deliver skipped: no approvalId');
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const dlClient = getDLClient(ctx?.accountId || DEFAULT_ACCOUNT_ID);
|
|
47
|
+
if (!dlClient) {
|
|
48
|
+
logger.warn?.('approval deliver skipped: no dlink client');
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await dlClient.pushApprovalEvent({
|
|
53
|
+
approvalId: info.approvalId,
|
|
54
|
+
kind: info.kind,
|
|
55
|
+
agentId: info.agentId,
|
|
56
|
+
sessionKey: info.sessionKey,
|
|
57
|
+
command: info.command,
|
|
58
|
+
cwd: info.cwd,
|
|
59
|
+
expiresAtMs: info.expiresAtMs
|
|
60
|
+
});
|
|
61
|
+
logger.info?.('approval forwarded: id=' + info.approvalId + ' agent=' + info.agentId);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
logger.warn?.('approval forward failed: ' + String(e));
|
|
65
|
+
}
|
|
66
|
+
return { approvalId: info.approvalId, deliveredAt: Date.now() };
|
|
67
|
+
},
|
|
68
|
+
updateEntry: async () => { },
|
|
69
|
+
deleteEntry: async () => { }
|
|
70
|
+
},
|
|
71
|
+
interactions: {}
|
|
72
|
+
};
|
|
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
|
+
}
|
package/dist/src/channel.js
CHANGED
|
@@ -4,6 +4,7 @@ import { checkConfigured, deleteDLAccount, listDLConfiguredAccountIds, resolveDe
|
|
|
4
4
|
import { formatPairingApproveHint } from "openclaw/plugin-sdk/core";
|
|
5
5
|
import { getDLLogger } from "./runtime.js";
|
|
6
6
|
import { dlOutbound, dlPairing } from "./outbound.js";
|
|
7
|
+
import { createDLApprovalCapability } from "./approval-native.js";
|
|
7
8
|
export const channelMeta = pkg.openclaw.channel;
|
|
8
9
|
export const LOG_TAG = channelMeta.selectionLabel;
|
|
9
10
|
const channelId = channelMeta.id;
|
|
@@ -142,6 +143,8 @@ export const dlChannel = {
|
|
|
142
143
|
},
|
|
143
144
|
pairing: dlPairing(channelId),
|
|
144
145
|
outbound: dlOutbound(),
|
|
146
|
+
// 2026-10-27 原生审批投递:core 把请求交给插件,插件转交中继扇出
|
|
147
|
+
approvalCapability: createDLApprovalCapability(),
|
|
145
148
|
gateway: {
|
|
146
149
|
startAccount: async (ctx) => {
|
|
147
150
|
ctx.setStatus({
|
|
@@ -25,6 +25,7 @@ export declare const REPLY_SEND_TIMEOUT_MS = 15000;
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const AUTO_RELAY_API = "/guest/relayBot/sendNotification";
|
|
27
27
|
export declare const AUTO_RELAY_PERSONA = "/guest/relayBot/persona";
|
|
28
|
+
export declare const AUTO_RELAY_APPROVAL_EVENT = "/guest/relayBot/approvalEvent";
|
|
28
29
|
export declare const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
29
30
|
export declare const FILE_LIMITS: {
|
|
30
31
|
image: {
|
package/dist/src/client/const.js
CHANGED
|
@@ -26,6 +26,7 @@ export const REPLY_SEND_TIMEOUT_MS = 15_000;
|
|
|
26
26
|
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
|
+
export const AUTO_RELAY_APPROVAL_EVENT = '/guest/relayBot/approvalEvent';
|
|
29
30
|
export const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
30
31
|
// ====== 上传文件配置常量(根据服务器性能调整)=======
|
|
31
32
|
export const FILE_LIMITS = {
|
|
@@ -53,6 +53,8 @@ export declare class DLClient extends EventEmitter {
|
|
|
53
53
|
* 失败返回 null,调用方降级为不注入,不影响正常对话。
|
|
54
54
|
*/
|
|
55
55
|
fetchPersona(sessionKey: string, agentId?: string): Promise<any | null>;
|
|
56
|
+
/** 2026-10-27 把审批请求交给中继(由中继扇出给最多三人) */
|
|
57
|
+
pushApprovalEvent(payload: Record<string, unknown>): Promise<any>;
|
|
56
58
|
private fetchConfig;
|
|
57
59
|
private changeConnectStatus;
|
|
58
60
|
private connect;
|
|
@@ -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_UPLOAD } from './const.js';
|
|
6
|
+
import { AUTO_RELAY_API, AUTO_RELAY_PERSONA, AUTO_RELAY_APPROVAL_EVENT, AUTO_RELAY_UPLOAD } 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,17 @@ export class DLClient extends EventEmitter {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/** 2026-10-27 把审批请求交给中继(由中继扇出给最多三人) */
|
|
70
|
+
async pushApprovalEvent(payload) {
|
|
71
|
+
try {
|
|
72
|
+
const data = await this.http.post(AUTO_RELAY_APPROVAL_EVENT, payload);
|
|
73
|
+
return data ?? null;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
this.logger.warn(`pushApprovalEvent failed: ${e}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
69
80
|
async fetchConfig() {
|
|
70
81
|
const url = `/guest/mqtt/getConfig/${this.ctx.account.config.apiKey}`;
|
|
71
82
|
const res = await this.http.get(url);
|
|
@@ -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({
|
|
@@ -1158,10 +1162,13 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
1158
1162
|
}
|
|
1159
1163
|
},
|
|
1160
1164
|
async onItemEvent(payload) {
|
|
1161
|
-
|
|
1165
|
+
let kind = payload.kind;
|
|
1162
1166
|
if (!kind) {
|
|
1163
1167
|
return;
|
|
1164
1168
|
}
|
|
1169
|
+
if (kind === 'preamble') {
|
|
1170
|
+
kind = 'commentary'; //适配前端中间文本气泡显示
|
|
1171
|
+
}
|
|
1165
1172
|
const content = [];
|
|
1166
1173
|
if (payload.name) {
|
|
1167
1174
|
content.push(payload.name);
|
|
@@ -1175,18 +1182,28 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
1175
1182
|
if (payload.summary) {
|
|
1176
1183
|
content.push(payload.summary);
|
|
1177
1184
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1185
|
+
if (kind === 'commentary') {
|
|
1186
|
+
await client.publishProcessingState({
|
|
1187
|
+
streamId,
|
|
1188
|
+
content: content.join('<br/>'),
|
|
1189
|
+
status: 'read',
|
|
1190
|
+
kind: kind
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
const messages = [
|
|
1195
|
+
`[tag_status_call_${kind}]`,
|
|
1196
|
+
`[tag_status_phase_${payload.phase}]`,
|
|
1197
|
+
`[tag_status_${payload.status}]`,
|
|
1198
|
+
...content
|
|
1199
|
+
];
|
|
1200
|
+
await client.publishProcessingState({
|
|
1201
|
+
streamId,
|
|
1202
|
+
content: messages.join('>'),
|
|
1203
|
+
status: 'read',
|
|
1204
|
+
kind: kind
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1190
1207
|
logger.info(`onItemEvent >>> kind=${payload.kind}, phase=${payload.phase}, status =${payload.status}, content=${content.join('>')}`);
|
|
1191
1208
|
},
|
|
1192
1209
|
onCommandOutput: async (payload) => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function mapMode(mode) {
|
|
5
|
+
switch (mode) {
|
|
6
|
+
case 'full': return 'full';
|
|
7
|
+
case 'deny': return 'read-only';
|
|
8
|
+
case 'ask': return 'guarded';
|
|
9
|
+
case 'auto': return 'workspace';
|
|
10
|
+
case 'allowlist': return 'read-only';
|
|
11
|
+
default: return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function readEffectiveExecPreset() {
|
|
15
|
+
try {
|
|
16
|
+
const file = process.env.OPENCLAW_CONFIG_PATH
|
|
17
|
+
|| path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
18
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
19
|
+
const exec = (cfg && cfg.tools && cfg.tools.exec) || {};
|
|
20
|
+
const byMode = mapMode(exec.mode);
|
|
21
|
+
if (byMode) {
|
|
22
|
+
return byMode;
|
|
23
|
+
}
|
|
24
|
+
const security = exec.security;
|
|
25
|
+
const ask = exec.ask;
|
|
26
|
+
if (security === 'full')
|
|
27
|
+
return 'full';
|
|
28
|
+
if (security === 'deny')
|
|
29
|
+
return 'read-only';
|
|
30
|
+
if (security === 'allowlist') {
|
|
31
|
+
return ask === 'on-miss' ? 'guarded' : 'read-only';
|
|
32
|
+
}
|
|
33
|
+
// OpenClaw 未配置时默认 full(与 openclaw approvals get 的 effective 一致)
|
|
34
|
+
return 'full';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -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 推送):
|
|
@@ -24,6 +25,9 @@ export class CmdHandler {
|
|
|
24
25
|
}, (payload) => {
|
|
25
26
|
// 2026-10-22:用量上报走服务端事件,由中继落库
|
|
26
27
|
void this.dlClient.publishEventToServer(payload, 'onUsage', '1');
|
|
28
|
+
}, (payload) => {
|
|
29
|
+
// 2026-10-29 审批推送:优先走 HTTP(可拿到审批人姓名),失败退回 MQTT 事件
|
|
30
|
+
void announceWaiting(this.dlClient, payload);
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
log(msg, type) {
|
|
@@ -18,8 +18,13 @@ export declare class ClawServices extends EventEmitter {
|
|
|
18
18
|
private readonly terminalManager;
|
|
19
19
|
/** 2026-10-22:token 用量上报(中继落库,按同事聚合) */
|
|
20
20
|
private readonly usagePublisher?;
|
|
21
|
+
private readonly approvalPublisher?;
|
|
22
|
+
private approvalTimer;
|
|
23
|
+
private readonly approvalSeen;
|
|
21
24
|
private usageTimer;
|
|
22
|
-
constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink, usagePublisher?: (payload: Record<string, unknown>) => void);
|
|
25
|
+
constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink, usagePublisher?: (payload: Record<string, unknown>) => void, approvalPublisher?: (payload: Record<string, unknown>) => void);
|
|
26
|
+
/** 2026-10-27 每 10 秒本地查一次审批队列,出现新条目才推中继(推模式,非中继拉取) */
|
|
27
|
+
private startApprovalWatcher;
|
|
23
28
|
/** 2026-10-22:每 60 秒把最近一天的 token 用量上报中继(幂等 upsert) */
|
|
24
29
|
private startUsageReporter;
|
|
25
30
|
log(msg: string, type: 'info' | 'warn' | 'error' | 'debug'): void;
|
|
@@ -127,6 +132,16 @@ export declare class ClawServices extends EventEmitter {
|
|
|
127
132
|
* 新增 cmd 通道 agent.stop 指令
|
|
128
133
|
*/
|
|
129
134
|
private handleStopAgent;
|
|
135
|
+
/** 2026-10-27 能力协议:声明本品牌支持的能力,客户端/中继据此渲染,不硬编码品牌 */
|
|
136
|
+
private handleAgentCapabilities;
|
|
137
|
+
/** 档位 id → 品牌真实取值;default 用 null 表示清除会话级覆盖 */
|
|
138
|
+
private static readonly PERMISSION_PRESETS;
|
|
139
|
+
private handlePermissionsGet;
|
|
140
|
+
private handlePermissionsSet;
|
|
141
|
+
/** 2026-10-27 审批待办(只读):三类审批合并,归一化为品牌无关结构 */
|
|
142
|
+
private handleApprovalsPending;
|
|
143
|
+
/** 2026-10-27 审批决议:品牌无关入口,内部映射到网关 approval.resolve */
|
|
144
|
+
private handleApprovalsResolve;
|
|
130
145
|
private handleSessionsAbort;
|
|
131
146
|
private handlChatAbout;
|
|
132
147
|
/**
|
|
@@ -9,6 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import WSClient from './ws-client.js';
|
|
11
11
|
import { getOpenclawPath, getExtCapabilitiesDir } from '../utils.js';
|
|
12
|
+
import { readEffectiveExecPreset } from '../exec-policy.js';
|
|
12
13
|
import { applyModelOverrideToSessionEntry } from 'openclaw/plugin-sdk/model-session-runtime';
|
|
13
14
|
// 2026-10-15 适配 openclaw 2026.10.3:config-runtime 子路径已被官方移除(2026-09-30 SDK owner 批准),
|
|
14
15
|
// 下列符号现由 session-store-runtime 导出(同名、签名兼容:resolveStorePath/getSessionEntry/listSessionEntries 仍为同步,patchSessionEntry 为 async,调用处均已 await)
|
|
@@ -32,14 +33,67 @@ export class ClawServices extends EventEmitter {
|
|
|
32
33
|
terminalManager;
|
|
33
34
|
/** 2026-10-22:token 用量上报(中继落库,按同事聚合) */
|
|
34
35
|
usagePublisher;
|
|
36
|
+
// 2026-10-27 审批事件推送(插件 → 中继),本地轮询发现新待审即上报
|
|
37
|
+
approvalPublisher;
|
|
38
|
+
approvalTimer = null;
|
|
39
|
+
approvalSeen = new Set();
|
|
35
40
|
usageTimer = null;
|
|
36
|
-
constructor(options, logger, terminalSink, usagePublisher) {
|
|
41
|
+
constructor(options, logger, terminalSink, usagePublisher, approvalPublisher) {
|
|
37
42
|
super();
|
|
38
43
|
this.options = options;
|
|
39
44
|
this.logger = logger;
|
|
40
45
|
this.usagePublisher = usagePublisher;
|
|
46
|
+
this.approvalPublisher = approvalPublisher;
|
|
41
47
|
this.terminalManager = new TerminalManager(terminalSink ?? (() => { }), (msg, type) => this.log(msg, type ?? 'info'));
|
|
42
48
|
}
|
|
49
|
+
/** 2026-10-27 每 10 秒本地查一次审批队列,出现新条目才推中继(推模式,非中继拉取) */
|
|
50
|
+
startApprovalWatcher() {
|
|
51
|
+
if (this.approvalTimer || !this.approvalPublisher) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const tick = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const publish = this.approvalPublisher;
|
|
57
|
+
if (!publish) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!this.client) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const kinds = ['exec', 'plugin', 'system-agent'];
|
|
64
|
+
for (const kind of kinds) {
|
|
65
|
+
const res = await this.client.approvalList(kind);
|
|
66
|
+
const list = Array.isArray(res) ? res : (res?.approvals ?? res?.items ?? []);
|
|
67
|
+
for (const item of list) {
|
|
68
|
+
const id = item?.id ?? item?.approvalId ?? item?.approval?.id;
|
|
69
|
+
if (!id || this.approvalSeen.has(String(id))) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
this.approvalSeen.add(String(id));
|
|
73
|
+
const pushed = await publish({
|
|
74
|
+
approvalId: String(id),
|
|
75
|
+
kind,
|
|
76
|
+
agentId: item?.agentId ?? item?.request?.agentId ?? '',
|
|
77
|
+
sessionKey: item?.sessionKey ?? item?.request?.sessionKey ?? '',
|
|
78
|
+
command: item?.command ?? item?.request?.command ?? item?.payload?.command ?? '',
|
|
79
|
+
cwd: item?.cwd ?? item?.request?.cwd,
|
|
80
|
+
createdAtMs: item?.createdAtMs ?? 0,
|
|
81
|
+
expiresAtMs: item?.expiresAtMs ?? 0
|
|
82
|
+
});
|
|
83
|
+
void pushed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (this.approvalSeen.size > 500) {
|
|
87
|
+
this.approvalSeen.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
this.log(`approval watch failed: ${String(e)}`, 'warn');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
this.approvalTimer = setInterval(() => { void tick(); }, 10000);
|
|
95
|
+
this.approvalTimer.unref?.();
|
|
96
|
+
}
|
|
43
97
|
/** 2026-10-22:每 60 秒把最近一天的 token 用量上报中继(幂等 upsert) */
|
|
44
98
|
startUsageReporter() {
|
|
45
99
|
if (this.usageTimer || !this.usagePublisher) {
|
|
@@ -148,6 +202,7 @@ export class ClawServices extends EventEmitter {
|
|
|
148
202
|
this.isInitialized = true;
|
|
149
203
|
// 2026-10-22:启动后开始周期上报用量
|
|
150
204
|
this.startUsageReporter();
|
|
205
|
+
this.startApprovalWatcher();
|
|
151
206
|
return true;
|
|
152
207
|
}
|
|
153
208
|
catch (error) {
|
|
@@ -314,6 +369,22 @@ export class ClawServices extends EventEmitter {
|
|
|
314
369
|
// 2026-10-24:会话级中止,无 runId 时也能立即停止,并丢弃排队后续
|
|
315
370
|
result = await this.handleSessionsAbort(cmdMessage.params);
|
|
316
371
|
break;
|
|
372
|
+
// 2026-10-27 能力协议(品牌无关):能力发现 + 执行档位
|
|
373
|
+
case 'agent.capabilities':
|
|
374
|
+
result = this.handleAgentCapabilities();
|
|
375
|
+
break;
|
|
376
|
+
case 'permissions.get':
|
|
377
|
+
result = await this.handlePermissionsGet(cmdMessage.params ?? {});
|
|
378
|
+
break;
|
|
379
|
+
case 'permissions.set':
|
|
380
|
+
result = await this.handlePermissionsSet(cmdMessage.params ?? {});
|
|
381
|
+
break;
|
|
382
|
+
case 'approvals.resolve':
|
|
383
|
+
result = await this.handleApprovalsResolve(cmdMessage.params ?? {});
|
|
384
|
+
break;
|
|
385
|
+
case 'approvals.pending':
|
|
386
|
+
result = await this.handleApprovalsPending(cmdMessage.params ?? {});
|
|
387
|
+
break;
|
|
317
388
|
// 2026-10-21:终端能力(owner-only,见 CmdHandler 校验)
|
|
318
389
|
case 'terminal.open':
|
|
319
390
|
result = await this.terminalManager.open(cmdMessage.params);
|
|
@@ -972,12 +1043,137 @@ export class ClawServices extends EventEmitter {
|
|
|
972
1043
|
sessionKey
|
|
973
1044
|
});
|
|
974
1045
|
}
|
|
975
|
-
|
|
976
|
-
|
|
1046
|
+
/** 2026-10-27 能力协议:声明本品牌支持的能力,客户端/中继据此渲染,不硬编码品牌 */
|
|
1047
|
+
handleAgentCapabilities() {
|
|
1048
|
+
return {
|
|
1049
|
+
permissions: {
|
|
1050
|
+
supported: true,
|
|
1051
|
+
presets: [
|
|
1052
|
+
{ id: 'default', label: '预设', description: '遵循代理程式已设定的执行权限', level: 1 },
|
|
1053
|
+
{ id: 'read-only', label: '唯读', description: '工具可读取工作阶段根目录内的内容,但无法写入或执行命令', level: 2 },
|
|
1054
|
+
{ id: 'guarded', label: '防护模式', description: '超出工作阶段根目录范围的请求由人工审查', level: 3 },
|
|
1055
|
+
{ id: 'workspace', label: '工作区', description: '超出工作阶段根目录范围的请求由 AI 审查', level: 4 },
|
|
1056
|
+
{ id: 'full', label: '完整存取权', description: '无需审查,档案存取与命令执行皆不受限制', level: 5, requiresAdmin: true }
|
|
1057
|
+
]
|
|
1058
|
+
},
|
|
1059
|
+
approvals: { supported: true, push: true, kinds: ['exec', 'plugin', 'system-agent'] },
|
|
1060
|
+
audit: { supported: true }
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/** 档位 id → 品牌真实取值;default 用 null 表示清除会话级覆盖 */
|
|
1064
|
+
static PERMISSION_PRESETS = {
|
|
1065
|
+
'default': null,
|
|
1066
|
+
'read-only': 'read-only',
|
|
1067
|
+
'guarded': 'guarded',
|
|
1068
|
+
'workspace': 'workspace',
|
|
1069
|
+
'full': 'full'
|
|
1070
|
+
};
|
|
1071
|
+
async handlePermissionsGet(params) {
|
|
1072
|
+
const sessionKey = params?.sessionKey ?? params?.key;
|
|
977
1073
|
if (!sessionKey) {
|
|
978
|
-
throw new Error('
|
|
1074
|
+
throw new Error('permissions.get 缺少 sessionKey');
|
|
1075
|
+
}
|
|
1076
|
+
const row = await this.client?.sessionsDescribe({ key: sessionKey });
|
|
1077
|
+
const mode = row?.permissionMode ?? row?.session?.permissionMode ?? null;
|
|
1078
|
+
const presetId = Object.entries(ClawServices.PERMISSION_PRESETS).find(([, v]) => v === mode)?.[0] ?? 'default';
|
|
1079
|
+
// 2026-10-27 继承态:读一次生效策略,把「预设」解析成实际档位(客户端显示 预设(完整存取权))
|
|
1080
|
+
let effectivePresetId = presetId;
|
|
1081
|
+
// 2026-10-27 继承态:直接读网关配置解析实际档位(RPC exec.approvals.get 在插件里会静默失败)
|
|
1082
|
+
if (presetId === 'default') {
|
|
1083
|
+
const resolved = readEffectiveExecPreset();
|
|
1084
|
+
if (resolved) {
|
|
1085
|
+
effectivePresetId = resolved;
|
|
1086
|
+
}
|
|
979
1087
|
}
|
|
980
|
-
|
|
1088
|
+
this.log(`permissions.get session=${sessionKey} mode=${mode} preset=${presetId} effective=${effectivePresetId}`, 'info');
|
|
1089
|
+
return { presetId, inherited: !mode, effectivePresetId };
|
|
1090
|
+
}
|
|
1091
|
+
async handlePermissionsSet(params) {
|
|
1092
|
+
const sessionKey = params?.sessionKey ?? params?.key;
|
|
1093
|
+
const presetId = String(params?.presetId ?? '');
|
|
1094
|
+
if (!sessionKey || !presetId) {
|
|
1095
|
+
throw new Error('permissions.set 缺少 sessionKey 或 presetId');
|
|
1096
|
+
}
|
|
1097
|
+
if (!(presetId in ClawServices.PERMISSION_PRESETS)) {
|
|
1098
|
+
throw new Error('未知档位: ' + presetId);
|
|
1099
|
+
}
|
|
1100
|
+
const mode = ClawServices.PERMISSION_PRESETS[presetId];
|
|
1101
|
+
// 2026-10-27 写入后校验:网关可能返回 success=false 却不抛异常,也可能接受了但没落库
|
|
1102
|
+
const res = await this.client?.sessionsPatch({ key: sessionKey, patch: { permissionMode: mode } });
|
|
1103
|
+
if (res && res.success === false) {
|
|
1104
|
+
throw new Error(`permissions.set failed: ${res.error ?? res.code ?? 'unknown'}`);
|
|
1105
|
+
}
|
|
1106
|
+
const row = await this.client?.sessionsDescribe({ key: sessionKey });
|
|
1107
|
+
const applied = row?.permissionMode ?? row?.session?.permissionMode ?? null;
|
|
1108
|
+
if (mode !== null && applied !== mode) {
|
|
1109
|
+
throw new Error(`permissions.set not applied: expected=${mode} actual=${applied}`);
|
|
1110
|
+
}
|
|
1111
|
+
this.log(`permissions.set session=${sessionKey} preset=${presetId} mode=${mode} applied=${applied}`, 'info');
|
|
1112
|
+
return { ok: true, presetId };
|
|
1113
|
+
}
|
|
1114
|
+
/** 2026-10-27 审批待办(只读):三类审批合并,归一化为品牌无关结构 */
|
|
1115
|
+
async handleApprovalsPending(params) {
|
|
1116
|
+
const agentId = params?.agentId ? String(params.agentId) : undefined;
|
|
1117
|
+
const limit = Number(params?.limit ?? 50);
|
|
1118
|
+
const kinds = ['exec', 'plugin', 'system-agent'];
|
|
1119
|
+
const lists = await Promise.all(kinds.map(async (kind) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const res = await this.client?.approvalList(kind);
|
|
1122
|
+
const list = Array.isArray(res) ? res : (res?.approvals ?? res?.items ?? []);
|
|
1123
|
+
return list.map((it) => ({ ...it, __kind: kind }));
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
// 某一类审批不可用不影响其余类别
|
|
1127
|
+
return [];
|
|
1128
|
+
}
|
|
1129
|
+
}));
|
|
1130
|
+
const approvals = lists.flat()
|
|
1131
|
+
.filter((it) => !agentId || it.agentId === agentId || it?.request?.agentId === agentId)
|
|
1132
|
+
.slice(0, limit)
|
|
1133
|
+
.map((it) => ({
|
|
1134
|
+
approvalId: it.id ?? it.approvalId ?? it?.approval?.id,
|
|
1135
|
+
kind: it.__kind,
|
|
1136
|
+
agentId: it.agentId ?? it?.request?.agentId ?? '',
|
|
1137
|
+
agentName: it.agentName ?? it?.request?.agentName,
|
|
1138
|
+
sessionKey: it.sessionKey ?? it?.request?.sessionKey ?? '',
|
|
1139
|
+
requesterEmpId: it.requesterEmpId ?? it?.request?.requesterEmpId ?? '',
|
|
1140
|
+
command: it.command ?? it?.request?.command ?? it?.payload?.command ?? '',
|
|
1141
|
+
cwd: it.cwd ?? it?.request?.cwd,
|
|
1142
|
+
risk: it.risk ?? it?.request?.risk,
|
|
1143
|
+
createdAtMs: it.createdAtMs ?? it?.request?.createdAtMs ?? 0,
|
|
1144
|
+
expiresAtMs: it.expiresAtMs ?? it?.request?.expiresAtMs ?? 0,
|
|
1145
|
+
status: it.status ?? 'pending'
|
|
1146
|
+
}));
|
|
1147
|
+
return { approvals };
|
|
1148
|
+
}
|
|
1149
|
+
/** 2026-10-27 审批决议:品牌无关入口,内部映射到网关 approval.resolve */
|
|
1150
|
+
async handleApprovalsResolve(params) {
|
|
1151
|
+
const id = params?.approvalId ?? params?.id;
|
|
1152
|
+
const kind = params?.kind ?? 'exec';
|
|
1153
|
+
const decision = params?.decision;
|
|
1154
|
+
if (!id || !decision) {
|
|
1155
|
+
throw new Error('approvals.resolve 缺少 approvalId 或 decision');
|
|
1156
|
+
}
|
|
1157
|
+
if (!['exec', 'plugin', 'system-agent'].includes(String(kind))) {
|
|
1158
|
+
throw new Error('未知审批类型: ' + kind);
|
|
1159
|
+
}
|
|
1160
|
+
if (!['allow-once', 'allow-always', 'deny'].includes(String(decision))) {
|
|
1161
|
+
throw new Error('未知决议: ' + decision);
|
|
1162
|
+
}
|
|
1163
|
+
const res = await this.client?.resolveApproval({ id: String(id), kind: kind, decision: decision });
|
|
1164
|
+
return { ok: true, approvalId: id, decision, upstream: res ?? null };
|
|
1165
|
+
}
|
|
1166
|
+
async handleSessionsAbort(params) {
|
|
1167
|
+
// 2026-10-27:兼容客户端 key / sessionKey 两种写法,统一按网关 schema 转发
|
|
1168
|
+
const key = params?.key ?? params?.sessionKey;
|
|
1169
|
+
if (!key) {
|
|
1170
|
+
throw new Error('sessions.abort 缺少 key 参数');
|
|
1171
|
+
}
|
|
1172
|
+
return await this.client?.sessionsAbort({
|
|
1173
|
+
key,
|
|
1174
|
+
runId: params?.runId,
|
|
1175
|
+
clearQueued: params?.clearQueued ?? true
|
|
1176
|
+
});
|
|
981
1177
|
}
|
|
982
1178
|
async handlChatAbout(params) {
|
|
983
1179
|
const { sessionKey, runId } = params;
|
|
@@ -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' | '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' | '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';
|
|
@@ -316,7 +316,7 @@ export interface ClawMessage {
|
|
|
316
316
|
payload: any;
|
|
317
317
|
error?: any;
|
|
318
318
|
}
|
|
319
|
-
export type EventType = 'onTick' | 'onHealth' | 'onHandshaked' | 'onConnectState' | 'onError' | 'onAgentReply' | 'onAgentChat' | 'onChatTrajectory' | 'onCron' | 'onShutdown' | 'onSessionsChanged' | 'onSkillInstalled' | 'onSkillInstalling' | 'onSkillError' | 'onPairingRequest' | 'onPairingResult'
|
|
319
|
+
export type EventType = 'onApproval' | 'onTick' | 'onHealth' | 'onHandshaked' | 'onConnectState' | 'onError' | 'onAgentReply' | 'onAgentChat' | 'onChatTrajectory' | 'onCron' | 'onShutdown' | 'onSessionsChanged' | 'onSkillInstalled' | 'onSkillInstalling' | 'onSkillError' | 'onPairingRequest' | 'onPairingResult'
|
|
320
320
|
/** 2026-10-22:周期上报 token 用量 */
|
|
321
321
|
| 'onUsage' | 'onTerminal';
|
|
322
322
|
export type CmdParamsPatchSessionEntry = {
|
|
@@ -143,9 +143,22 @@ export declare class WSClient extends EventEmitter {
|
|
|
143
143
|
sessionKey: string;
|
|
144
144
|
runId?: string;
|
|
145
145
|
}): Promise<any>;
|
|
146
|
+
/** 2026-10-27 审批决议:放行一次 / 永久放行 / 拒绝(网关 schema: id/kind/decision) */
|
|
147
|
+
resolveApproval(params: {
|
|
148
|
+
id: string;
|
|
149
|
+
kind: 'exec' | 'plugin' | 'system-agent';
|
|
150
|
+
decision: 'allow-once' | 'allow-always' | 'deny';
|
|
151
|
+
}): Promise<any>;
|
|
152
|
+
/** 2026-10-27 读取生效执行策略:把「预设」解析成实际档位 */
|
|
153
|
+
execApprovalsGet(): Promise<any>;
|
|
154
|
+
/** 2026-10-27 审批队列(只读):三类审批分别取列表,core 已做过期过滤 */
|
|
155
|
+
approvalList(kind: 'exec' | 'plugin' | 'system-agent'): Promise<any>;
|
|
146
156
|
/** 2026-10-24:会话级中止(无 runId 时使用),同时丢弃排队后续 */
|
|
147
157
|
sessionsAbort(params: {
|
|
148
|
-
sessionKey
|
|
158
|
+
sessionKey?: string;
|
|
159
|
+
key?: string;
|
|
160
|
+
runId?: string;
|
|
161
|
+
clearQueued?: boolean;
|
|
149
162
|
}): Promise<any>;
|
|
150
163
|
getSkillSecurityVerdicts(agentId?: string): Promise<any>;
|
|
151
164
|
getUsageCost(params: Record<string, unknown>): Promise<any>;
|
|
@@ -464,13 +464,36 @@ export class WSClient extends EventEmitter {
|
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
466
|
}
|
|
467
|
+
/** 2026-10-27 审批决议:放行一次 / 永久放行 / 拒绝(网关 schema: id/kind/decision) */
|
|
468
|
+
async resolveApproval(params) {
|
|
469
|
+
return this.sendRequest({ type: 'req', method: 'approval.resolve', params });
|
|
470
|
+
}
|
|
471
|
+
/** 2026-10-27 读取生效执行策略:把「预设」解析成实际档位 */
|
|
472
|
+
async execApprovalsGet() {
|
|
473
|
+
return this.sendRequest({ type: 'req', method: 'exec.approvals.get', params: {} });
|
|
474
|
+
}
|
|
475
|
+
/** 2026-10-27 审批队列(只读):三类审批分别取列表,core 已做过期过滤 */
|
|
476
|
+
async approvalList(kind) {
|
|
477
|
+
const method = kind === 'exec'
|
|
478
|
+
? 'exec.approval.list'
|
|
479
|
+
: kind === 'plugin'
|
|
480
|
+
? 'plugin.approval.list'
|
|
481
|
+
: 'openclaw.approval.list';
|
|
482
|
+
return this.sendRequest({ type: 'req', method, params: {} });
|
|
483
|
+
}
|
|
467
484
|
/** 2026-10-24:会话级中止(无 runId 时使用),同时丢弃排队后续 */
|
|
468
485
|
async sessionsAbort(params) {
|
|
469
|
-
|
|
486
|
+
// 2026-10-27 修正:sessions.abort 的参数字段是 key(不是 sessionKey),网关 schema 为 key/runId/agentId/clearQueued;
|
|
487
|
+
// clearQueued 丢弃排队后续,避免中止后不到一秒又被排队任务拉起。
|
|
488
|
+
const key = params.key ?? params.sessionKey;
|
|
470
489
|
return this.sendRequest({
|
|
471
490
|
type: 'req',
|
|
472
491
|
method: 'sessions.abort',
|
|
473
|
-
params: {
|
|
492
|
+
params: {
|
|
493
|
+
key,
|
|
494
|
+
clearQueued: params.clearQueued ?? true,
|
|
495
|
+
...(params.runId ? { runId: params.runId } : {})
|
|
496
|
+
}
|
|
474
497
|
});
|
|
475
498
|
}
|
|
476
499
|
async getSkillSecurityVerdicts(agentId) {
|
|
@@ -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
|
+
}
|