mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Add configurable group session scope (default user_in_chat).
Ops bots now isolate per-speaker Harness sessions in groups, with a settings/RPC toggle to restore shared chat sessions when needed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d10a941fc8
commit
7f1b9bd480
30 changed files with 798 additions and 282 deletions
23
FORK.md
23
FORK.md
|
|
@ -3,7 +3,7 @@
|
||||||
本仓库是 [`@xmanrui/dsh-im@4.9.1`](https://github.com/xmanrui/dsh-im) 的 **完整 fork**(九渠道 + AI Office 均保留),用于 Netx / 运维场景下自控:
|
本仓库是 [`@xmanrui/dsh-im@4.9.1`](https://github.com/xmanrui/dsh-im) 的 **完整 fork**(九渠道 + AI Office 均保留),用于 Netx / 运维场景下自控:
|
||||||
|
|
||||||
- 访问控制(入站白名单 / 群策略)
|
- 访问控制(入站白名单 / 群策略)
|
||||||
- 会话策略(例如群聊拆个人 Session)
|
- 会话策略(群聊按发言人拆分 Session,可配置)
|
||||||
- 通告 / 投递默认与文案
|
- 通告 / 投递默认与文案
|
||||||
|
|
||||||
上游 remote 名为 `upstream`。不要与社区包 `@xmanrui/dsh-im` 同时装进同一 profile。
|
上游 remote 名为 `upstream`。不要与社区包 `@xmanrui/dsh-im` 同时装进同一 profile。
|
||||||
|
|
@ -19,21 +19,26 @@
|
||||||
cd D:\project\chatgpt\dsh-im-ops
|
cd D:\project\chatgpt\dsh-im-ops
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
dsh plugin --profile web add -w "D:\project\chatgpt\dsh-im-ops"
|
dsh plugin --profile web add -w "github:hansjone/dsh-im-ops"
|
||||||
|
# 或本地:dsh plugin --profile web add -w "D:\project\chatgpt\dsh-im-ops"
|
||||||
# 重启 dsh web
|
# 重启 dsh web
|
||||||
```
|
```
|
||||||
|
|
||||||
包名:`dsh-im-ops@4.9.1-ops.0`(cordis id:`dsh-im-ops`)。
|
包名:`dsh-im-ops@4.9.1-ops.1`(cordis id:`dsh-im-ops`)。
|
||||||
|
|
||||||
扫码态一般仍在 `~/.dsh/integrations/…`;换包后若异常,在 IM 设置里重新关联设备。
|
扫码态一般仍在 `~/.dsh/integrations/…`;换包后若异常,在 IM 设置里重新关联设备。
|
||||||
|
|
||||||
## 第一刀改动方向(尚未改业务逻辑)
|
## 已落地的运维改动
|
||||||
|
|
||||||
1. WhatsApp / 通用 access mode:运维友好的默认与设置文案
|
1. **群 Session 策略**(对齐 oclaw `user_in_chat`)
|
||||||
2. 群 Session key:对齐 oclaw `user_in_chat`(可配置)
|
- 默认:`user_in_chat` → 群会话 key 为 `group:<chatId>:user:<senderId>`
|
||||||
3. 投递默认目标 / 与 netxops 协作说明
|
- 可选:`chat` → 整群共享 `group:<chatId>`(上游行为)
|
||||||
|
- 在「访问设置」页可改;RPC:`bot.group-session-scope.set`
|
||||||
|
- 当前对 **WhatsApp / Telegram / Slack / Discord**(TextHarness 路径)生效;其它渠道的同策略接线后续补齐
|
||||||
|
|
||||||
业务改动前先 `npm run build` 冒烟扫码与私聊。
|
2. **访问控制**仍由本 fork 持久化(`workspaces.json` 的 `accessPolicies`),设置 UI 与上游同构,运维可直接改白名单 / open 模式
|
||||||
|
|
||||||
|
3. **主动群通告**继续用既有 delivery(`botId + targetId`),与入站 Session 策略独立
|
||||||
|
|
||||||
## 同步上游
|
## 同步上游
|
||||||
|
|
||||||
|
|
@ -41,3 +46,5 @@ dsh plugin --profile web add -w "D:\project\chatgpt\dsh-im-ops"
|
||||||
git fetch upstream --tags
|
git fetch upstream --tags
|
||||||
git merge v4.9.x # 或 cherry-pick;冲突自行解决后再 build
|
git merge v4.9.x # 或 cherry-pick;冲突自行解决后再 build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
改源码后务必 `npm run build` 并推送含 `lib/` 的提交,否则 GitHub 安装会加载过期 bundle。
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ var React23 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
name: "dsh-im-ops",
|
||||||
version: "4.9.1-ops.0",
|
version: "4.9.1-ops.1",
|
||||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
@ -11797,7 +11797,29 @@ var React21 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
// plugin-src/client/access-policy-settings.js
|
// plugin-src/client/access-policy-settings.js
|
||||||
var React20 = __toESM(require("react"), 1);
|
var React20 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
|
// src/channels/shared/session-scope.mjs
|
||||||
|
var GROUP_SESSION_SCOPES = Object.freeze(["chat", "user_in_chat"]);
|
||||||
|
var DEFAULT_GROUP_SESSION_SCOPE = "user_in_chat";
|
||||||
|
function normalizeGroupSessionScope(value) {
|
||||||
|
const scope = String(value ?? "").trim().toLowerCase();
|
||||||
|
if (scope === "chat" || scope === "shared" || scope === "shared_chat") return "chat";
|
||||||
|
if (scope === "user_in_chat" || scope === "user" || scope === "per_user" || scope === "member") {
|
||||||
|
return "user_in_chat";
|
||||||
|
}
|
||||||
|
return DEFAULT_GROUP_SESSION_SCOPE;
|
||||||
|
}
|
||||||
|
function validateGroupSessionScope(value) {
|
||||||
|
const scope = String(value ?? "").trim().toLowerCase();
|
||||||
|
if (scope === "chat" || scope === "user_in_chat") return scope;
|
||||||
|
const error = new TypeError(`Invalid group session scope: ${String(value)}`);
|
||||||
|
error.code = "invalid-group-session-scope";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugin-src/client/access-policy-settings.js
|
||||||
var ACCESS_POLICY_ENDPOINT = "bot.access-policy.set";
|
var ACCESS_POLICY_ENDPOINT = "bot.access-policy.set";
|
||||||
|
var GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
|
||||||
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||||
weixin: Object.freeze({
|
weixin: Object.freeze({
|
||||||
directUserLabel: "\u5FAE\u4FE1\u7528\u6237 ID",
|
directUserLabel: "\u5FAE\u4FE1\u7528\u6237 ID",
|
||||||
|
|
@ -12092,15 +12114,18 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
const definition = ACCESS_CHANNEL_DEFINITIONS[channel4];
|
const definition = ACCESS_CHANNEL_DEFINITIONS[channel4];
|
||||||
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
|
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
|
||||||
const initialKey = JSON.stringify(initialPolicy);
|
const initialKey = JSON.stringify(initialPolicy);
|
||||||
|
const initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
|
||||||
const [draft, setDraft] = React20.useState(() => clonePolicy(
|
const [draft, setDraft] = React20.useState(() => clonePolicy(
|
||||||
initialPolicy ?? DEFAULT_ACCESS_POLICY
|
initialPolicy ?? DEFAULT_ACCESS_POLICY
|
||||||
));
|
));
|
||||||
|
const [groupSessionScope, setGroupSessionScope] = React20.useState(initialScope);
|
||||||
const [saving, setSaving] = React20.useState(false);
|
const [saving, setSaving] = React20.useState(false);
|
||||||
const [feedback, setFeedback] = React20.useState(null);
|
const [feedback, setFeedback] = React20.useState(null);
|
||||||
React20.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
const next = normalizeAccessPolicy(account?.accessPolicy);
|
const next = normalizeAccessPolicy(account?.accessPolicy);
|
||||||
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
||||||
}, [account?.botId, initialKey]);
|
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
||||||
|
}, [account?.botId, initialKey, account?.groupSessionScope]);
|
||||||
React20.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
}, [account?.botId]);
|
}, [account?.botId]);
|
||||||
|
|
@ -12117,16 +12142,25 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const policy = validateAccessPolicy(draft);
|
const policy = validateAccessPolicy(draft);
|
||||||
|
const scope = validateGroupSessionScope(groupSessionScope);
|
||||||
if (typeof rpcCall !== "function") throw new Error("\u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
|
if (typeof rpcCall !== "function") throw new Error("\u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
|
||||||
const value = unwrapRpcResult10(await rpcCall(ACCESS_POLICY_ENDPOINT, {
|
const value = unwrapRpcResult10(await rpcCall(ACCESS_POLICY_ENDPOINT, {
|
||||||
botId: account.botId,
|
botId: account.botId,
|
||||||
policy
|
policy
|
||||||
}));
|
}));
|
||||||
|
const scopeValue = unwrapRpcResult10(await rpcCall(GROUP_SESSION_SCOPE_ENDPOINT, {
|
||||||
|
botId: account.botId,
|
||||||
|
groupSessionScope: scope
|
||||||
|
}));
|
||||||
const saved = policyFromSnapshot(value, account.botId);
|
const saved = policyFromSnapshot(value, account.botId);
|
||||||
if (!saved) throw new Error("\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u5DF2\u4FDD\u5B58\u7684\u8BBF\u95EE\u7B56\u7565\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
|
if (!saved) throw new Error("\u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u5DF2\u4FDD\u5B58\u7684\u8BBF\u95EE\u7B56\u7565\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
|
||||||
|
const savedScope = normalizeGroupSessionScope(
|
||||||
|
scopeValue?.snapshot?.bots?.find?.((bot) => bot?.botId === account.botId)?.groupSessionScope ?? scopeValue?.bots?.find?.((bot) => bot?.botId === account.botId)?.groupSessionScope ?? scope
|
||||||
|
);
|
||||||
setDraft(clonePolicy(saved));
|
setDraft(clonePolicy(saved));
|
||||||
onSaved?.(saved);
|
setGroupSessionScope(savedScope);
|
||||||
setFeedback({ tone: "success", message: "\u8BBF\u95EE\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002" });
|
onSaved?.({ ...saved, groupSessionScope: savedScope });
|
||||||
|
setFeedback({ tone: "success", message: "\u8BBF\u95EE\u4E0E\u7FA4\u4F1A\u8BDD\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFeedback({
|
setFeedback({
|
||||||
tone: "error",
|
tone: "error",
|
||||||
|
|
@ -12172,6 +12206,59 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
definition.groupSupported === false ? null : h2(
|
||||||
|
"fieldset",
|
||||||
|
{
|
||||||
|
className: "dim-accessScene",
|
||||||
|
disabled: saving,
|
||||||
|
"data-scene": "group-session",
|
||||||
|
"aria-label": localizeText("\u7FA4\u4F1A\u8BDD\u7B56\u7565")
|
||||||
|
},
|
||||||
|
h2(
|
||||||
|
"legend",
|
||||||
|
null,
|
||||||
|
h2(
|
||||||
|
"span",
|
||||||
|
{ className: "dim-accessLegendContent" },
|
||||||
|
h2("span", null, "\u7FA4\u4F1A\u8BDD\u7B56\u7565"),
|
||||||
|
h2(
|
||||||
|
"span",
|
||||||
|
{ className: "dim-channelHelp dim-accessLegendHelp" },
|
||||||
|
h2("button", {
|
||||||
|
type: "button",
|
||||||
|
className: "dim-channelHelpButton",
|
||||||
|
"aria-label": localizeText("\u67E5\u770B\u7FA4\u4F1A\u8BDD\u7B56\u7565\u8BF4\u660E")
|
||||||
|
}, h2("span", { "aria-hidden": true }, "?")),
|
||||||
|
h2("span", {
|
||||||
|
className: "dim-channelTooltip dim-accessHelpTooltip",
|
||||||
|
role: "tooltip"
|
||||||
|
}, "\u8FD0\u7EF4\u9ED8\u8BA4\u6309\u53D1\u8A00\u4EBA\u62C6\u5206 Session\uFF0C\u907F\u514D\u540C\u7FA4\u591A\u4EBA\u4E92\u76F8\u6253\u65AD\uFF1B\u53EF\u9009\u6539\u56DE\u6574\u7FA4\u5171\u4EAB Session\u3002")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h2(
|
||||||
|
"div",
|
||||||
|
{ className: "dim-accessControls" },
|
||||||
|
h2(
|
||||||
|
"label",
|
||||||
|
{ className: "dim-accessField" },
|
||||||
|
h2("span", null, "\u7FA4\u5185 Session \u7ED1\u5B9A"),
|
||||||
|
h2(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
value: groupSessionScope || DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
"aria-label": localizeText("\u7FA4\u5185 Session \u7ED1\u5B9A"),
|
||||||
|
onChange: (event) => {
|
||||||
|
setGroupSessionScope(event.target.value);
|
||||||
|
setFeedback(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h2("option", { value: "user_in_chat" }, "\u6309\u53D1\u8A00\u4EBA\u62C6\u5206\uFF08\u63A8\u8350\uFF09"),
|
||||||
|
h2("option", { value: "chat" }, "\u6574\u7FA4\u5171\u4EAB\u4E00\u4E2A Session")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
feedback ? h2("p", {
|
feedback ? h2("p", {
|
||||||
className: "dim-accessFeedback",
|
className: "dim-accessFeedback",
|
||||||
"data-tone": feedback.tone,
|
"data-tone": feedback.tone,
|
||||||
|
|
@ -12186,7 +12273,7 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
className: "dim-deliveryButton",
|
className: "dim-deliveryButton",
|
||||||
"data-kind": "primary",
|
"data-kind": "primary",
|
||||||
disabled: saving || !initialPolicy
|
disabled: saving || !initialPolicy
|
||||||
}, saving ? "\u6B63\u5728\u4FDD\u5B58\u2026" : "\u4FDD\u5B58\u8BBF\u95EE\u8BBE\u7F6E")
|
}, saving ? "\u6B63\u5728\u4FDD\u5B58\u2026" : "\u4FDD\u5B58\u8BBF\u95EE\u4E0E\u4F1A\u8BDD\u8BBE\u7F6E")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
524
lib/index.js
524
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-im-ops",
|
"name": "dsh-im-ops",
|
||||||
"version": "4.9.1-ops.0",
|
"version": "4.9.1-ops.1",
|
||||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,15 @@ import {
|
||||||
normalizeAccessPolicy,
|
normalizeAccessPolicy,
|
||||||
validateAccessPolicy,
|
validateAccessPolicy,
|
||||||
} from '../../src/channels/shared/access-policy.mjs';
|
} from '../../src/channels/shared/access-policy.mjs';
|
||||||
|
import {
|
||||||
|
DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
normalizeGroupSessionScope,
|
||||||
|
validateGroupSessionScope,
|
||||||
|
} from '../../src/channels/shared/session-scope.mjs';
|
||||||
import { h, localizeText } from './i18n.js';
|
import { h, localizeText } from './i18n.js';
|
||||||
|
|
||||||
export const ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
|
export const ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
|
||||||
|
export const GROUP_SESSION_SCOPE_ENDPOINT = 'bot.group-session-scope.set';
|
||||||
|
|
||||||
export const ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
export const ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||||
weixin: Object.freeze({
|
weixin: Object.freeze({
|
||||||
|
|
@ -262,16 +268,19 @@ export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved })
|
||||||
const definition = ACCESS_CHANNEL_DEFINITIONS[channel];
|
const definition = ACCESS_CHANNEL_DEFINITIONS[channel];
|
||||||
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
|
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
|
||||||
const initialKey = JSON.stringify(initialPolicy);
|
const initialKey = JSON.stringify(initialPolicy);
|
||||||
|
const initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
|
||||||
const [draft, setDraft] = React.useState(() => clonePolicy(
|
const [draft, setDraft] = React.useState(() => clonePolicy(
|
||||||
initialPolicy ?? DEFAULT_ACCESS_POLICY,
|
initialPolicy ?? DEFAULT_ACCESS_POLICY,
|
||||||
));
|
));
|
||||||
|
const [groupSessionScope, setGroupSessionScope] = React.useState(initialScope);
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
const [feedback, setFeedback] = React.useState(null);
|
const [feedback, setFeedback] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const next = normalizeAccessPolicy(account?.accessPolicy);
|
const next = normalizeAccessPolicy(account?.accessPolicy);
|
||||||
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
||||||
}, [account?.botId, initialKey]);
|
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
||||||
|
}, [account?.botId, initialKey, account?.groupSessionScope]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
|
|
@ -288,16 +297,27 @@ export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved })
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const policy = validateAccessPolicy(draft);
|
const policy = validateAccessPolicy(draft);
|
||||||
|
const scope = validateGroupSessionScope(groupSessionScope);
|
||||||
if (typeof rpcCall !== 'function') throw new Error('访问设置暂不可用。');
|
if (typeof rpcCall !== 'function') throw new Error('访问设置暂不可用。');
|
||||||
const value = unwrapRpcResult(await rpcCall(ACCESS_POLICY_ENDPOINT, {
|
const value = unwrapRpcResult(await rpcCall(ACCESS_POLICY_ENDPOINT, {
|
||||||
botId: account.botId,
|
botId: account.botId,
|
||||||
policy,
|
policy,
|
||||||
}));
|
}));
|
||||||
|
const scopeValue = unwrapRpcResult(await rpcCall(GROUP_SESSION_SCOPE_ENDPOINT, {
|
||||||
|
botId: account.botId,
|
||||||
|
groupSessionScope: scope,
|
||||||
|
}));
|
||||||
const saved = policyFromSnapshot(value, account.botId);
|
const saved = policyFromSnapshot(value, account.botId);
|
||||||
if (!saved) throw new Error('服务没有返回已保存的访问策略,请刷新后重试。');
|
if (!saved) throw new Error('服务没有返回已保存的访问策略,请刷新后重试。');
|
||||||
|
const savedScope = normalizeGroupSessionScope(
|
||||||
|
scopeValue?.snapshot?.bots?.find?.((bot) => bot?.botId === account.botId)?.groupSessionScope
|
||||||
|
?? scopeValue?.bots?.find?.((bot) => bot?.botId === account.botId)?.groupSessionScope
|
||||||
|
?? scope,
|
||||||
|
);
|
||||||
setDraft(clonePolicy(saved));
|
setDraft(clonePolicy(saved));
|
||||||
onSaved?.(saved);
|
setGroupSessionScope(savedScope);
|
||||||
setFeedback({ tone: 'success', message: '访问设置已保存。' });
|
onSaved?.({ ...saved, groupSessionScope: savedScope });
|
||||||
|
setFeedback({ tone: 'success', message: '访问与群会话设置已保存。' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFeedback({
|
setFeedback({
|
||||||
tone: 'error',
|
tone: 'error',
|
||||||
|
|
@ -335,6 +355,40 @@ export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved })
|
||||||
unsupported: definition.groupSupported === false,
|
unsupported: definition.groupSupported === false,
|
||||||
onChange: (group) => { setDraft((current) => ({ ...current, group })); setFeedback(null); },
|
onChange: (group) => { setDraft((current) => ({ ...current, group })); setFeedback(null); },
|
||||||
}),
|
}),
|
||||||
|
definition.groupSupported === false
|
||||||
|
? null
|
||||||
|
: h('fieldset', {
|
||||||
|
className: 'dim-accessScene',
|
||||||
|
disabled: saving,
|
||||||
|
'data-scene': 'group-session',
|
||||||
|
'aria-label': localizeText('群会话策略'),
|
||||||
|
},
|
||||||
|
h('legend', null,
|
||||||
|
h('span', { className: 'dim-accessLegendContent' },
|
||||||
|
h('span', null, '群会话策略'),
|
||||||
|
h('span', { className: 'dim-channelHelp dim-accessLegendHelp' },
|
||||||
|
h('button', {
|
||||||
|
type: 'button',
|
||||||
|
className: 'dim-channelHelpButton',
|
||||||
|
'aria-label': localizeText('查看群会话策略说明'),
|
||||||
|
}, h('span', { 'aria-hidden': true }, '?')),
|
||||||
|
h('span', {
|
||||||
|
className: 'dim-channelTooltip dim-accessHelpTooltip',
|
||||||
|
role: 'tooltip',
|
||||||
|
}, '运维默认按发言人拆分 Session,避免同群多人互相打断;可选改回整群共享 Session。')))),
|
||||||
|
h('div', { className: 'dim-accessControls' },
|
||||||
|
h('label', { className: 'dim-accessField' },
|
||||||
|
h('span', null, '群内 Session 绑定'),
|
||||||
|
h('select', {
|
||||||
|
value: groupSessionScope || DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
'aria-label': localizeText('群内 Session 绑定'),
|
||||||
|
onChange: (event) => {
|
||||||
|
setGroupSessionScope(event.target.value);
|
||||||
|
setFeedback(null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
h('option', { value: 'user_in_chat' }, '按发言人拆分(推荐)'),
|
||||||
|
h('option', { value: 'chat' }, '整群共享一个 Session'))))),
|
||||||
feedback ? h('p', {
|
feedback ? h('p', {
|
||||||
className: 'dim-accessFeedback',
|
className: 'dim-accessFeedback',
|
||||||
'data-tone': feedback.tone,
|
'data-tone': feedback.tone,
|
||||||
|
|
@ -347,5 +401,5 @@ export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved })
|
||||||
className: 'dim-deliveryButton',
|
className: 'dim-deliveryButton',
|
||||||
'data-kind': 'primary',
|
'data-kind': 'primary',
|
||||||
disabled: saving || !initialPolicy,
|
disabled: saving || !initialPolicy,
|
||||||
}, saving ? '正在保存…' : '保存访问设置')));
|
}, saving ? '正在保存…' : '保存访问与会话设置')));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'dingtalk', config: botConfig,
|
channel: 'dingtalk', config: botConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
maxMessageChars: config.maxMessageChars ?? 4_000,
|
maxMessageChars: config.maxMessageChars ?? 4_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 15_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 15_000,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
|
|
@ -22,6 +23,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
approveSender: 'bot.sender.approve',
|
approveSender: 'bot.sender.approve',
|
||||||
revokeSender: 'bot.sender.revoke',
|
revokeSender: 'bot.sender.revoke',
|
||||||
});
|
});
|
||||||
|
|
@ -106,6 +108,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === DINGTALK_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
||||||
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
|
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
|
||||||
&& validId(payload.botId)
|
&& validId(payload.botId)
|
||||||
|
|
@ -294,6 +300,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
||||||
value = await controller.updateAccessPolicy(
|
value = await controller.updateAccessPolicy(
|
||||||
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === DINGTALK_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId, payload.groupSessionScope, (status) => publicStatus(status, cachedEncode),
|
||||||
|
);
|
||||||
} else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await publicStatus(
|
value = await publicStatus(
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, id, {
|
accessPolicy: accessPolicyProvider(workspaces, id, {
|
||||||
channel: 'feishu', config: botConfig,
|
channel: 'feishu', config: botConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(id) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
slashCommands: config.slashCommands !== false,
|
slashCommands: config.slashCommands !== false,
|
||||||
...(wsAgent ? { wsAgent } : {}),
|
...(wsAgent ? { wsAgent } : {}),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace
|
||||||
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||||
import {
|
import {
|
||||||
isFeishuGroupResponseMode,
|
isFeishuGroupResponseMode,
|
||||||
|
|
@ -24,6 +25,7 @@ import {
|
||||||
export const FEISHU_ENDPOINTS = Object.freeze({
|
export const FEISHU_ENDPOINTS = Object.freeze({
|
||||||
...FEISHU_CLIENT_ENDPOINTS,
|
...FEISHU_CLIENT_ENDPOINTS,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
export { FEISHU_RPC_CHANNEL };
|
export { FEISHU_RPC_CHANNEL };
|
||||||
export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
|
export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
|
||||||
|
|
@ -433,6 +435,10 @@ function validPayload(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
|
if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
|
||||||
return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
|
return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
|
||||||
&& safeOpaqueId(payload.botId)
|
&& safeOpaqueId(payload.botId)
|
||||||
|
|
@ -696,6 +702,11 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
||||||
payload.botId, payload.policy,
|
payload.botId, payload.policy,
|
||||||
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
|
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === FEISHU_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId, payload.groupSessionScope, (status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
|
||||||
|
);
|
||||||
} else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await toPublicFeishuStatus(
|
value = await toPublicFeishuStatus(
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'qq', config: botConfig,
|
channel: 'qq', config: botConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
||||||
logger: {
|
logger: {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from '../../../../src/channels/shared/connection-test.mjs';
|
} from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
|
|
@ -22,6 +23,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
|
export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
|
||||||
|
|
||||||
|
|
@ -88,6 +90,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === QQ_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
return 'Unknown QQ endpoint.';
|
return 'Unknown QQ endpoint.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,6 +199,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
||||||
value = await controller.updateAccessPolicy(
|
value = await controller.updateAccessPolicy(
|
||||||
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === QQ_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId, payload.groupSessionScope, (status) => publicStatus(status, cachedEncode),
|
||||||
|
);
|
||||||
} else if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await publicStatus(
|
value = await publicStatus(
|
||||||
|
|
|
||||||
17
plugin-src/host/channels/shared/group-session-scope-rpc.mjs
Normal file
17
plugin-src/host/channels/shared/group-session-scope-rpc.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { validateGroupSessionScope } from '../../../../src/channels/shared/session-scope.mjs';
|
||||||
|
|
||||||
|
export const SET_GROUP_SESSION_SCOPE_ENDPOINT = 'bot.group-session-scope.set';
|
||||||
|
|
||||||
|
export function validGroupSessionScopePayload(payload) {
|
||||||
|
try {
|
||||||
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|
||||||
|
|| Reflect.ownKeys(payload).length !== 2
|
||||||
|
|| !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'groupSessionScope')
|
||||||
|
|| typeof payload.botId !== 'string'
|
||||||
|
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
||||||
|
validateGroupSessionScope(payload.groupSessionScope);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -121,6 +121,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
||||||
state: workspaceScope.state,
|
state: workspaceScope.state,
|
||||||
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
|
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, { channel, config: botConfig }),
|
accessPolicy: accessPolicyProvider(workspaces, botId, { channel, config: botConfig }),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
||||||
logger: {
|
logger: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from './group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -21,6 +22,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
|
const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
|
||||||
|
|
@ -83,6 +85,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === TOKEN_BOT_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
return 'Unknown bot endpoint.';
|
return 'Unknown bot endpoint.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,6 +176,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
||||||
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
|
||||||
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
||||||
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
||||||
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(payload.botId, payload.groupSessionScope);
|
||||||
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
|
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'slack', config: botConfig,
|
channel: 'slack', config: botConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
||||||
logger: {
|
logger: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -22,6 +23,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
|
export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
|
||||||
|
|
||||||
|
|
@ -87,6 +89,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === SLACK_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
return 'Unknown Slack endpoint.';
|
return 'Unknown Slack endpoint.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +180,10 @@ export function createSlackRpcHandler(controller) {
|
||||||
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
||||||
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
||||||
}
|
}
|
||||||
|
else if (endpoint === SLACK_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(payload.botId, payload.groupSessionScope);
|
||||||
|
}
|
||||||
else if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
|
else if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
|
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'wecom', config: botConfig,
|
channel: 'wecom', config: botConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
|
||||||
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
|
|
@ -22,6 +23,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
|
export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
|
||||||
|
|
||||||
|
|
@ -88,6 +90,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === WECOM_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
return 'Unknown Enterprise WeChat endpoint.';
|
return 'Unknown Enterprise WeChat endpoint.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +200,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
||||||
value = await controller.updateAccessPolicy(
|
value = await controller.updateAccessPolicy(
|
||||||
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === WECOM_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId, payload.groupSessionScope, (status) => publicStatus(status, cachedEncode),
|
||||||
|
);
|
||||||
} else if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await publicStatus(
|
value = await publicStatus(
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'weixin', config: accountConfig,
|
channel: 'weixin', config: accountConfig,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
maxMessageChars: config.maxMessageChars ?? DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
maxMessageChars: config.maxMessageChars ?? DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
||||||
logger: {
|
logger: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import {
|
import {
|
||||||
publicWorkspaceError,
|
publicWorkspaceError,
|
||||||
|
|
@ -29,6 +30,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
});
|
});
|
||||||
export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
|
export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
|
||||||
|
|
||||||
|
|
@ -95,6 +97,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === WEIXIN_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
return 'Unknown Weixin endpoint.';
|
return 'Unknown Weixin endpoint.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,6 +235,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
||||||
value = await controller.updateAccessPolicy(
|
value = await controller.updateAccessPolicy(
|
||||||
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === WEIXIN_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId, payload.groupSessionScope, (status) => publicStatus(status, cachedEncode),
|
||||||
|
);
|
||||||
} else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
|
} else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
|
||||||
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
||||||
value = await publicStatus(
|
value = await publicStatus(
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
||||||
channel: 'whatsapp', config: botConfig, equals: whatsappAccessPolicyIdsEqual,
|
channel: 'whatsapp', config: botConfig, equals: whatsappAccessPolicyIdsEqual,
|
||||||
}),
|
}),
|
||||||
|
groupSessionScope: { getScope: () => workspaces.groupSessionScopeFor(botId) },
|
||||||
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
||||||
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
|
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
|
||||||
createSession,
|
createSession,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QRCode from 'qrcode';
|
||||||
|
|
||||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
|
|
@ -16,6 +17,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
reconnectBot: 'bot.reconnect',
|
reconnectBot: 'bot.reconnect',
|
||||||
deleteBot: 'bot.delete',
|
deleteBot: 'bot.delete',
|
||||||
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
||||||
|
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_ENDPOINT,
|
||||||
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
|
|
@ -58,6 +60,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAccessPolicyPayload(payload)
|
return validAccessPolicyPayload(payload)
|
||||||
? null : '请提交有效的访问设置。';
|
? null : '请提交有效的访问设置。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
return validGroupSessionScopePayload(payload)
|
||||||
|
? null : '请提交有效的群会话策略。';
|
||||||
|
}
|
||||||
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
||||||
return validWorkspacePayload(payload)
|
return validWorkspacePayload(payload)
|
||||||
? null : '请输入工作区绝对路径。';
|
? null : '请输入工作区绝对路径。';
|
||||||
|
|
@ -190,6 +196,13 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
||||||
payload.policy,
|
payload.policy,
|
||||||
(status) => publicStatus(status, cachedEncode),
|
(status) => publicStatus(status, cachedEncode),
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setGroupSessionScope) {
|
||||||
|
if (typeof controller.updateGroupSessionScope !== 'function') throw new Error('Group session scope update is unavailable');
|
||||||
|
value = await controller.updateGroupSessionScope(
|
||||||
|
payload.botId,
|
||||||
|
payload.groupSessionScope,
|
||||||
|
(status) => publicStatus(status, cachedEncode),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,7 @@ export class DiscordRuntime {
|
||||||
#state;
|
#state;
|
||||||
#contextEnhancement;
|
#contextEnhancement;
|
||||||
#accessPolicy;
|
#accessPolicy;
|
||||||
|
#groupSessionScope;
|
||||||
#logger;
|
#logger;
|
||||||
#replyTimeoutMs;
|
#replyTimeoutMs;
|
||||||
#connectTimeoutMs;
|
#connectTimeoutMs;
|
||||||
|
|
@ -562,6 +563,7 @@ export class DiscordRuntime {
|
||||||
state,
|
state,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
accessPolicy,
|
accessPolicy,
|
||||||
|
groupSessionScope,
|
||||||
logger = console,
|
logger = console,
|
||||||
replyTimeoutMs = 600_000,
|
replyTimeoutMs = 600_000,
|
||||||
connectTimeoutMs = 20_000,
|
connectTimeoutMs = 20_000,
|
||||||
|
|
@ -579,6 +581,7 @@ export class DiscordRuntime {
|
||||||
this.#state = state;
|
this.#state = state;
|
||||||
this.#contextEnhancement = contextEnhancement;
|
this.#contextEnhancement = contextEnhancement;
|
||||||
this.#accessPolicy = accessPolicy;
|
this.#accessPolicy = accessPolicy;
|
||||||
|
this.#groupSessionScope = groupSessionScope;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
this.#replyTimeoutMs = replyTimeoutMs;
|
this.#replyTimeoutMs = replyTimeoutMs;
|
||||||
this.#connectTimeoutMs = connectTimeoutMs;
|
this.#connectTimeoutMs = connectTimeoutMs;
|
||||||
|
|
@ -659,6 +662,7 @@ export class DiscordRuntime {
|
||||||
state: this.#state,
|
state: this.#state,
|
||||||
contextEnhancement: this.#contextEnhancement,
|
contextEnhancement: this.#contextEnhancement,
|
||||||
accessPolicy: this.#accessPolicy,
|
accessPolicy: this.#accessPolicy,
|
||||||
|
groupSessionScope: this.#groupSessionScope,
|
||||||
status: this.#status,
|
status: this.#status,
|
||||||
logger: this.#logger,
|
logger: this.#logger,
|
||||||
replyTimeoutMs: this.#replyTimeoutMs,
|
replyTimeoutMs: this.#replyTimeoutMs,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ import {
|
||||||
normalizeAccessPolicy,
|
normalizeAccessPolicy,
|
||||||
validateAccessPolicy,
|
validateAccessPolicy,
|
||||||
} from './access-policy.mjs';
|
} from './access-policy.mjs';
|
||||||
|
import {
|
||||||
|
DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
normalizeGroupSessionScope,
|
||||||
|
validateGroupSessionScope,
|
||||||
|
} from './session-scope.mjs';
|
||||||
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
|
DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
|
||||||
|
|
@ -179,6 +184,19 @@ function normalizeDocument(value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let groupSessionScopes = {};
|
||||||
|
if (value.groupSessionScopes !== undefined) {
|
||||||
|
if (!value.groupSessionScopes || typeof value.groupSessionScopes !== 'object'
|
||||||
|
|| Array.isArray(value.groupSessionScopes)) return null;
|
||||||
|
for (const [botId, scope] of Object.entries(value.groupSessionScopes)) {
|
||||||
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return null;
|
||||||
|
try {
|
||||||
|
groupSessionScopes[botId] = validateGroupSessionScope(scope);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const contextEnhancement = Object.create(null);
|
const contextEnhancement = Object.create(null);
|
||||||
// Enhancement damage is isolated from the existing workspace/preset document.
|
// Enhancement damage is isolated from the existing workspace/preset document.
|
||||||
if (value.contextEnhancement && typeof value.contextEnhancement === 'object'
|
if (value.contextEnhancement && typeof value.contextEnhancement === 'object'
|
||||||
|
|
@ -200,6 +218,7 @@ function normalizeDocument(value) {
|
||||||
version,
|
version,
|
||||||
workspaces,
|
workspaces,
|
||||||
agentPresets,
|
agentPresets,
|
||||||
|
groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
accessPolicies,
|
accessPolicies,
|
||||||
|
|
@ -210,12 +229,14 @@ function storedDocument({
|
||||||
version,
|
version,
|
||||||
workspaces,
|
workspaces,
|
||||||
agentPresets,
|
agentPresets,
|
||||||
|
groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
accessPolicies,
|
accessPolicies,
|
||||||
}) {
|
}) {
|
||||||
const document = { version, workspaces };
|
const document = { version, workspaces };
|
||||||
if (Object.keys(agentPresets).length > 0) document.agentPresets = agentPresets;
|
if (Object.keys(agentPresets).length > 0) document.agentPresets = agentPresets;
|
||||||
|
if (Object.keys(groupSessionScopes).length > 0) document.groupSessionScopes = groupSessionScopes;
|
||||||
if (Object.keys(contextEnhancement).length > 0) {
|
if (Object.keys(contextEnhancement).length > 0) {
|
||||||
document.contextEnhancement = contextEnhancement;
|
document.contextEnhancement = contextEnhancement;
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +288,7 @@ export class BotWorkspaceStore {
|
||||||
#version = 1;
|
#version = 1;
|
||||||
#workspaces = {};
|
#workspaces = {};
|
||||||
#agentPresets = {};
|
#agentPresets = {};
|
||||||
|
#groupSessionScopes = {};
|
||||||
#contextEnhancement = {};
|
#contextEnhancement = {};
|
||||||
#deliveryTargets = Object.create(null);
|
#deliveryTargets = Object.create(null);
|
||||||
#accessPolicies = Object.create(null);
|
#accessPolicies = Object.create(null);
|
||||||
|
|
@ -293,6 +315,7 @@ export class BotWorkspaceStore {
|
||||||
this.#version = normalized.version;
|
this.#version = normalized.version;
|
||||||
this.#workspaces = normalized.workspaces;
|
this.#workspaces = normalized.workspaces;
|
||||||
this.#agentPresets = normalized.agentPresets;
|
this.#agentPresets = normalized.agentPresets;
|
||||||
|
this.#groupSessionScopes = normalized.groupSessionScopes;
|
||||||
this.#contextEnhancement = normalized.contextEnhancement;
|
this.#contextEnhancement = normalized.contextEnhancement;
|
||||||
this.#deliveryTargets = normalized.deliveryTargets;
|
this.#deliveryTargets = normalized.deliveryTargets;
|
||||||
this.#accessPolicies = normalized.accessPolicies;
|
this.#accessPolicies = normalized.accessPolicies;
|
||||||
|
|
@ -301,6 +324,7 @@ export class BotWorkspaceStore {
|
||||||
this.#version = 1;
|
this.#version = 1;
|
||||||
this.#workspaces = {};
|
this.#workspaces = {};
|
||||||
this.#agentPresets = {};
|
this.#agentPresets = {};
|
||||||
|
this.#groupSessionScopes = {};
|
||||||
this.#contextEnhancement = {};
|
this.#contextEnhancement = {};
|
||||||
this.#deliveryTargets = Object.create(null);
|
this.#deliveryTargets = Object.create(null);
|
||||||
this.#accessPolicies = Object.create(null);
|
this.#accessPolicies = Object.create(null);
|
||||||
|
|
@ -335,6 +359,14 @@ export class BotWorkspaceStore {
|
||||||
return this.#agentPresets[botIdOf(botId)] ?? null;
|
return this.#agentPresets[botIdOf(botId)] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupSessionScopeFor(botId) {
|
||||||
|
const id = botIdOf(botId);
|
||||||
|
if (this.has(id) && Object.hasOwn(this.#groupSessionScopes, id)) {
|
||||||
|
return normalizeGroupSessionScope(this.#groupSessionScopes[id]);
|
||||||
|
}
|
||||||
|
return DEFAULT_GROUP_SESSION_SCOPE;
|
||||||
|
}
|
||||||
|
|
||||||
contextEnhancementFor(botId) {
|
contextEnhancementFor(botId) {
|
||||||
const id = botIdOf(botId);
|
const id = botIdOf(botId);
|
||||||
return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
|
return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
|
||||||
|
|
@ -565,6 +597,38 @@ export class BotWorkspaceStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setGroupSessionScope(botId, value, { incarnation } = {}) {
|
||||||
|
const id = botIdOf(botId);
|
||||||
|
if (!this.has(id)
|
||||||
|
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
||||||
|
const error = new Error('找不到要修改的机器人。');
|
||||||
|
error.code = 'workspace-bot-not-found';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const scope = validateGroupSessionScope(value);
|
||||||
|
return this.#enqueue(id, async () => {
|
||||||
|
if (!this.has(id)
|
||||||
|
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
||||||
|
const error = new Error('找不到要修改的机器人。');
|
||||||
|
error.code = 'workspace-bot-not-found';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const previous = Object.hasOwn(this.#groupSessionScopes, id)
|
||||||
|
? this.#groupSessionScopes[id]
|
||||||
|
: undefined;
|
||||||
|
if (previous === scope) return scope;
|
||||||
|
this.#groupSessionScopes[id] = scope;
|
||||||
|
try {
|
||||||
|
await this.#persist();
|
||||||
|
} catch (error) {
|
||||||
|
if (previous === undefined) delete this.#groupSessionScopes[id];
|
||||||
|
else this.#groupSessionScopes[id] = previous;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async setContextEnhancement(botId, value, { incarnation } = {}) {
|
async setContextEnhancement(botId, value, { incarnation } = {}) {
|
||||||
const id = botIdOf(botId);
|
const id = botIdOf(botId);
|
||||||
const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
|
const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
|
||||||
|
|
@ -764,6 +828,7 @@ export class BotWorkspaceStore {
|
||||||
const candidates = new Set([
|
const candidates = new Set([
|
||||||
...Object.keys(this.#workspaces),
|
...Object.keys(this.#workspaces),
|
||||||
...Object.keys(this.#agentPresets),
|
...Object.keys(this.#agentPresets),
|
||||||
|
...Object.keys(this.#groupSessionScopes),
|
||||||
...Object.keys(this.#contextEnhancement),
|
...Object.keys(this.#contextEnhancement),
|
||||||
...Object.keys(this.#deliveryTargets),
|
...Object.keys(this.#deliveryTargets),
|
||||||
...Object.keys(this.#accessPolicies),
|
...Object.keys(this.#accessPolicies),
|
||||||
|
|
@ -783,6 +848,7 @@ export class BotWorkspaceStore {
|
||||||
...bot,
|
...bot,
|
||||||
workspace: this.workspaceFor(bot.botId),
|
workspace: this.workspaceFor(bot.botId),
|
||||||
agentPreset: this.agentPresetFor(bot.botId),
|
agentPreset: this.agentPresetFor(bot.botId),
|
||||||
|
groupSessionScope: this.groupSessionScopeFor(bot.botId),
|
||||||
contextEnhancement: this.contextEnhancementFor(bot.botId),
|
contextEnhancement: this.contextEnhancementFor(bot.botId),
|
||||||
accessPolicy: this.accessPolicyFor(bot.botId),
|
accessPolicy: this.accessPolicyFor(bot.botId),
|
||||||
}
|
}
|
||||||
|
|
@ -814,13 +880,15 @@ export class BotWorkspaceStore {
|
||||||
async #retireCurrentIncarnation(id) {
|
async #retireCurrentIncarnation(id) {
|
||||||
const hadWorkspace = Object.hasOwn(this.#workspaces, id);
|
const hadWorkspace = Object.hasOwn(this.#workspaces, id);
|
||||||
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
||||||
|
const hadGroupSessionScope = Object.hasOwn(this.#groupSessionScopes, id);
|
||||||
const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
|
const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
|
||||||
const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
|
const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
|
||||||
const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
|
const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
|
||||||
const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
|
const needsCleanup = hadWorkspace || hadPreset || hadGroupSessionScope || hadContextEnhancement
|
||||||
|| hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
|
|| hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
|
||||||
delete this.#workspaces[id];
|
delete this.#workspaces[id];
|
||||||
delete this.#agentPresets[id];
|
delete this.#agentPresets[id];
|
||||||
|
delete this.#groupSessionScopes[id];
|
||||||
delete this.#contextEnhancement[id];
|
delete this.#contextEnhancement[id];
|
||||||
delete this.#deliveryTargets[id];
|
delete this.#deliveryTargets[id];
|
||||||
delete this.#accessPolicies[id];
|
delete this.#accessPolicies[id];
|
||||||
|
|
@ -863,6 +931,7 @@ export class BotWorkspaceStore {
|
||||||
version,
|
version,
|
||||||
workspaces: this.#workspaces,
|
workspaces: this.#workspaces,
|
||||||
agentPresets: this.#agentPresets,
|
agentPresets: this.#agentPresets,
|
||||||
|
groupSessionScopes: this.#groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
accessPolicies,
|
accessPolicies,
|
||||||
|
|
@ -873,6 +942,7 @@ export class BotWorkspaceStore {
|
||||||
async #persistCurrentDocument() {
|
async #persistCurrentDocument() {
|
||||||
if (Object.keys(this.#workspaces).length > 0
|
if (Object.keys(this.#workspaces).length > 0
|
||||||
|| Object.keys(this.#agentPresets).length > 0
|
|| Object.keys(this.#agentPresets).length > 0
|
||||||
|
|| Object.keys(this.#groupSessionScopes).length > 0
|
||||||
|| Object.keys(this.#contextEnhancement).length > 0
|
|| Object.keys(this.#contextEnhancement).length > 0
|
||||||
|| Object.keys(this.#deliveryTargets).length > 0
|
|| Object.keys(this.#deliveryTargets).length > 0
|
||||||
|| Object.keys(this.#accessPolicies).length > 0) {
|
|| Object.keys(this.#accessPolicies).length > 0) {
|
||||||
|
|
@ -1427,6 +1497,30 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateGroupSessionScope = (botId, value, projectStatus) => {
|
||||||
|
const incarnation = workspaces.incarnationFor(botId);
|
||||||
|
const scope = validateGroupSessionScope(value);
|
||||||
|
return withBotTransition(botId, async () => {
|
||||||
|
const snapshot = await controller.status();
|
||||||
|
if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
|
||||||
|
const error = new Error('找不到要修改的机器人。');
|
||||||
|
error.code = 'workspace-bot-not-found';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
|
||||||
|
const decorated = workspaces.decorateStatus(snapshot);
|
||||||
|
const updated = {
|
||||||
|
...decorated,
|
||||||
|
bots: decorated.bots.map((bot) => bot?.botId === botId
|
||||||
|
? { ...bot, groupSessionScope: scope } : bot),
|
||||||
|
...(catalog ? { agentPresetCatalog: catalog } : {}),
|
||||||
|
};
|
||||||
|
const result = projectStatus ? await projectStatus(updated) : updated;
|
||||||
|
await workspaces.setGroupSessionScope(botId, scope, { incarnation });
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
||||||
// Fence the old runtime without changing the durable mapping. A crash
|
// Fence the old runtime without changing the durable mapping. A crash
|
||||||
// before the controller removes its config therefore keeps the bot's
|
// before the controller removes its config therefore keeps the bot's
|
||||||
|
|
@ -1468,6 +1562,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
||||||
if (property === 'updateAgentPreset') return updateAgentPreset;
|
if (property === 'updateAgentPreset') return updateAgentPreset;
|
||||||
if (property === 'updateContextEnhancement') return updateContextEnhancement;
|
if (property === 'updateContextEnhancement') return updateContextEnhancement;
|
||||||
if (property === 'updateAccessPolicy') return updateAccessPolicy;
|
if (property === 'updateAccessPolicy') return updateAccessPolicy;
|
||||||
|
if (property === 'updateGroupSessionScope') return updateGroupSessionScope;
|
||||||
const value = Reflect.get(target, property, target);
|
const value = Reflect.get(target, property, target);
|
||||||
if (typeof value !== 'function') return value;
|
if (typeof value !== 'function') return value;
|
||||||
if (property === 'deleteBot') {
|
if (property === 'deleteBot') {
|
||||||
|
|
|
||||||
61
src/channels/shared/session-scope.mjs
Normal file
61
src/channels/shared/session-scope.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Group conversation → Harness Session key policy (oclaw-aligned).
|
||||||
|
*
|
||||||
|
* - `chat`: one Session per group (`group:<conversationId>`) — upstream dsh-im default
|
||||||
|
* - `user_in_chat`: one Session per speaker in a group (`group:<conversationId>:user:<senderId>`)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const GROUP_SESSION_SCOPES = Object.freeze(['chat', 'user_in_chat']);
|
||||||
|
|
||||||
|
/** Ops-fork default: isolate concurrent operators in the same WhatsApp/ops group. */
|
||||||
|
export const DEFAULT_GROUP_SESSION_SCOPE = 'user_in_chat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {'chat' | 'user_in_chat'}
|
||||||
|
*/
|
||||||
|
export function normalizeGroupSessionScope(value) {
|
||||||
|
const scope = String(value ?? '').trim().toLowerCase();
|
||||||
|
if (scope === 'chat' || scope === 'shared' || scope === 'shared_chat') return 'chat';
|
||||||
|
if (scope === 'user_in_chat' || scope === 'user' || scope === 'per_user' || scope === 'member') {
|
||||||
|
return 'user_in_chat';
|
||||||
|
}
|
||||||
|
return DEFAULT_GROUP_SESSION_SCOPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} value
|
||||||
|
* @returns {'chat' | 'user_in_chat'}
|
||||||
|
*/
|
||||||
|
export function validateGroupSessionScope(value) {
|
||||||
|
const scope = String(value ?? '').trim().toLowerCase();
|
||||||
|
if (scope === 'chat' || scope === 'user_in_chat') return scope;
|
||||||
|
const error = new TypeError(`Invalid group session scope: ${String(value)}`);
|
||||||
|
error.code = 'invalid-group-session-scope';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the durable conversation key used for session binding and queues.
|
||||||
|
* @param {{ kind: string, conversationId: string, senderId: string, groupSessionScope?: string }} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function conversationKeyFor({
|
||||||
|
kind,
|
||||||
|
conversationId,
|
||||||
|
senderId,
|
||||||
|
groupSessionScope = DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
}) {
|
||||||
|
const conversation = String(conversationId ?? '').trim();
|
||||||
|
const sender = String(senderId ?? '').trim();
|
||||||
|
if (!conversation) throw new TypeError('conversationId is required');
|
||||||
|
if (kind === 'group') {
|
||||||
|
const scope = normalizeGroupSessionScope(groupSessionScope);
|
||||||
|
if (scope === 'user_in_chat') {
|
||||||
|
if (!sender) throw new TypeError('senderId is required for user_in_chat group sessions');
|
||||||
|
return `group:${conversation}:user:${sender}`;
|
||||||
|
}
|
||||||
|
return `group:${conversation}`;
|
||||||
|
}
|
||||||
|
return `direct:${conversation}`;
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,11 @@ import {
|
||||||
setLastMessageFailure,
|
setLastMessageFailure,
|
||||||
} from './message-failure.mjs';
|
} from './message-failure.mjs';
|
||||||
import { beginStatusReaction } from './status-reaction.mjs';
|
import { beginStatusReaction } from './status-reaction.mjs';
|
||||||
|
import {
|
||||||
|
conversationKeyFor,
|
||||||
|
DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
normalizeGroupSessionScope,
|
||||||
|
} from './session-scope.mjs';
|
||||||
|
|
||||||
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
||||||
const FILE_ONLY_COMPLETION_TEXT = '任务已完成。';
|
const FILE_ONLY_COMPLETION_TEXT = '任务已完成。';
|
||||||
|
|
@ -137,6 +142,8 @@ export class TextHarnessBridge {
|
||||||
#state;
|
#state;
|
||||||
#contextEnhancement;
|
#contextEnhancement;
|
||||||
#accessPolicy;
|
#accessPolicy;
|
||||||
|
/** @type {string | { getScope?: () => string }} */
|
||||||
|
#groupSessionScope;
|
||||||
#status;
|
#status;
|
||||||
#logger;
|
#logger;
|
||||||
#replyTimeoutMs;
|
#replyTimeoutMs;
|
||||||
|
|
@ -158,6 +165,7 @@ export class TextHarnessBridge {
|
||||||
state,
|
state,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
accessPolicy,
|
accessPolicy,
|
||||||
|
groupSessionScope = DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
status = createTextBridgeStatus(),
|
status = createTextBridgeStatus(),
|
||||||
logger = console,
|
logger = console,
|
||||||
replyTimeoutMs = 600_000,
|
replyTimeoutMs = 600_000,
|
||||||
|
|
@ -172,6 +180,7 @@ export class TextHarnessBridge {
|
||||||
this.#state = state;
|
this.#state = state;
|
||||||
this.#contextEnhancement = contextEnhancement;
|
this.#contextEnhancement = contextEnhancement;
|
||||||
this.#accessPolicy = accessPolicy;
|
this.#accessPolicy = accessPolicy;
|
||||||
|
this.#groupSessionScope = groupSessionScope;
|
||||||
this.#status = status;
|
this.#status = status;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
this.#replyTimeoutMs = replyTimeoutMs;
|
this.#replyTimeoutMs = replyTimeoutMs;
|
||||||
|
|
@ -186,6 +195,15 @@ export class TextHarnessBridge {
|
||||||
return structuredClone(this.#status);
|
return structuredClone(this.#status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#resolveGroupSessionScope() {
|
||||||
|
const configured = this.#groupSessionScope;
|
||||||
|
if (configured && typeof configured === 'object'
|
||||||
|
&& typeof configured.getScope === 'function') {
|
||||||
|
return normalizeGroupSessionScope(configured.getScope());
|
||||||
|
}
|
||||||
|
return normalizeGroupSessionScope(configured);
|
||||||
|
}
|
||||||
|
|
||||||
accept(message, { contextSnapshot, accessDecision } = {}) {
|
accept(message, { contextSnapshot, accessDecision } = {}) {
|
||||||
if (this.#signal?.aborted) return Promise.resolve();
|
if (this.#signal?.aborted) return Promise.resolve();
|
||||||
const conversationId = cleanText(message?.conversationId);
|
const conversationId = cleanText(message?.conversationId);
|
||||||
|
|
@ -262,7 +280,12 @@ export class TextHarnessBridge {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = `${normalized.kind}:${normalized.conversationId}`;
|
const key = conversationKeyFor({
|
||||||
|
kind: normalized.kind,
|
||||||
|
conversationId: normalized.conversationId,
|
||||||
|
senderId,
|
||||||
|
groupSessionScope: this.#resolveGroupSessionScope(),
|
||||||
|
});
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
const text = cleanText(normalized.content);
|
const text = cleanText(normalized.content);
|
||||||
const batchCommand = isBatchInputCommand(text);
|
const batchCommand = isBatchInputCommand(text);
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,7 @@ export class SlackRuntime {
|
||||||
#state;
|
#state;
|
||||||
#contextEnhancement;
|
#contextEnhancement;
|
||||||
#accessPolicy;
|
#accessPolicy;
|
||||||
|
#groupSessionScope;
|
||||||
#logger;
|
#logger;
|
||||||
#replyTimeoutMs;
|
#replyTimeoutMs;
|
||||||
#connectTimeoutMs;
|
#connectTimeoutMs;
|
||||||
|
|
@ -432,6 +433,7 @@ export class SlackRuntime {
|
||||||
state,
|
state,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
accessPolicy,
|
accessPolicy,
|
||||||
|
groupSessionScope,
|
||||||
logger = console,
|
logger = console,
|
||||||
replyTimeoutMs = 600_000,
|
replyTimeoutMs = 600_000,
|
||||||
connectTimeoutMs = 20_000,
|
connectTimeoutMs = 20_000,
|
||||||
|
|
@ -449,6 +451,7 @@ export class SlackRuntime {
|
||||||
this.#state = state;
|
this.#state = state;
|
||||||
this.#contextEnhancement = contextEnhancement;
|
this.#contextEnhancement = contextEnhancement;
|
||||||
this.#accessPolicy = accessPolicy;
|
this.#accessPolicy = accessPolicy;
|
||||||
|
this.#groupSessionScope = groupSessionScope;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
this.#replyTimeoutMs = replyTimeoutMs;
|
this.#replyTimeoutMs = replyTimeoutMs;
|
||||||
this.#connectTimeoutMs = connectTimeoutMs;
|
this.#connectTimeoutMs = connectTimeoutMs;
|
||||||
|
|
@ -527,6 +530,7 @@ export class SlackRuntime {
|
||||||
state: this.#state,
|
state: this.#state,
|
||||||
contextEnhancement: this.#contextEnhancement,
|
contextEnhancement: this.#contextEnhancement,
|
||||||
accessPolicy: this.#accessPolicy,
|
accessPolicy: this.#accessPolicy,
|
||||||
|
groupSessionScope: this.#groupSessionScope,
|
||||||
status: this.#status,
|
status: this.#status,
|
||||||
logger: this.#logger,
|
logger: this.#logger,
|
||||||
replyTimeoutMs: this.#replyTimeoutMs,
|
replyTimeoutMs: this.#replyTimeoutMs,
|
||||||
|
|
|
||||||
|
|
@ -768,6 +768,7 @@ export class TelegramRuntime {
|
||||||
#state;
|
#state;
|
||||||
#contextEnhancement;
|
#contextEnhancement;
|
||||||
#accessPolicy;
|
#accessPolicy;
|
||||||
|
#groupSessionScope;
|
||||||
#logger;
|
#logger;
|
||||||
#replyTimeoutMs;
|
#replyTimeoutMs;
|
||||||
#createApi;
|
#createApi;
|
||||||
|
|
@ -787,6 +788,7 @@ export class TelegramRuntime {
|
||||||
state,
|
state,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
accessPolicy,
|
accessPolicy,
|
||||||
|
groupSessionScope,
|
||||||
logger = console,
|
logger = console,
|
||||||
replyTimeoutMs = 600_000,
|
replyTimeoutMs = 600_000,
|
||||||
createApi = (options) => new TelegramApi(options),
|
createApi = (options) => new TelegramApi(options),
|
||||||
|
|
@ -801,6 +803,7 @@ export class TelegramRuntime {
|
||||||
this.#state = state;
|
this.#state = state;
|
||||||
this.#contextEnhancement = contextEnhancement;
|
this.#contextEnhancement = contextEnhancement;
|
||||||
this.#accessPolicy = accessPolicy;
|
this.#accessPolicy = accessPolicy;
|
||||||
|
this.#groupSessionScope = groupSessionScope;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
this.#replyTimeoutMs = replyTimeoutMs;
|
this.#replyTimeoutMs = replyTimeoutMs;
|
||||||
this.#createApi = createApi;
|
this.#createApi = createApi;
|
||||||
|
|
@ -902,6 +905,7 @@ export class TelegramRuntime {
|
||||||
state: this.#state,
|
state: this.#state,
|
||||||
contextEnhancement: this.#contextEnhancement,
|
contextEnhancement: this.#contextEnhancement,
|
||||||
accessPolicy: this.#accessPolicy,
|
accessPolicy: this.#accessPolicy,
|
||||||
|
groupSessionScope: this.#groupSessionScope,
|
||||||
status: this.#status,
|
status: this.#status,
|
||||||
logger: this.#logger,
|
logger: this.#logger,
|
||||||
replyTimeoutMs: this.#replyTimeoutMs,
|
replyTimeoutMs: this.#replyTimeoutMs,
|
||||||
|
|
|
||||||
|
|
@ -625,6 +625,7 @@ export class WhatsappRuntime {
|
||||||
#state;
|
#state;
|
||||||
#contextEnhancement;
|
#contextEnhancement;
|
||||||
#accessPolicy;
|
#accessPolicy;
|
||||||
|
#groupSessionScope;
|
||||||
#logger;
|
#logger;
|
||||||
#replyTimeoutMs;
|
#replyTimeoutMs;
|
||||||
#connectTimeoutMs;
|
#connectTimeoutMs;
|
||||||
|
|
@ -644,6 +645,7 @@ export class WhatsappRuntime {
|
||||||
state,
|
state,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
accessPolicy,
|
accessPolicy,
|
||||||
|
groupSessionScope,
|
||||||
logger = console,
|
logger = console,
|
||||||
replyTimeoutMs = 600_000,
|
replyTimeoutMs = 600_000,
|
||||||
connectTimeoutMs = 30_000,
|
connectTimeoutMs = 30_000,
|
||||||
|
|
@ -659,6 +661,7 @@ export class WhatsappRuntime {
|
||||||
this.#state = state;
|
this.#state = state;
|
||||||
this.#contextEnhancement = contextEnhancement;
|
this.#contextEnhancement = contextEnhancement;
|
||||||
this.#accessPolicy = accessPolicy;
|
this.#accessPolicy = accessPolicy;
|
||||||
|
this.#groupSessionScope = groupSessionScope;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
this.#replyTimeoutMs = replyTimeoutMs;
|
this.#replyTimeoutMs = replyTimeoutMs;
|
||||||
this.#connectTimeoutMs = connectTimeoutMs;
|
this.#connectTimeoutMs = connectTimeoutMs;
|
||||||
|
|
@ -756,6 +759,7 @@ export class WhatsappRuntime {
|
||||||
} : {}),
|
} : {}),
|
||||||
equals: whatsappAccessPolicyIdsEqual,
|
equals: whatsappAccessPolicyIdsEqual,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
groupSessionScope: this.#groupSessionScope,
|
||||||
status: this.#status,
|
status: this.#status,
|
||||||
logger: this.#logger,
|
logger: this.#logger,
|
||||||
replyTimeoutMs: this.#replyTimeoutMs,
|
replyTimeoutMs: this.#replyTimeoutMs,
|
||||||
|
|
|
||||||
61
test/channels/shared/session-scope.test.mjs
Normal file
61
test/channels/shared/session-scope.test.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
conversationKeyFor,
|
||||||
|
DEFAULT_GROUP_SESSION_SCOPE,
|
||||||
|
normalizeGroupSessionScope,
|
||||||
|
validateGroupSessionScope,
|
||||||
|
} from '../../../src/channels/shared/session-scope.mjs';
|
||||||
|
|
||||||
|
describe('session-scope', () => {
|
||||||
|
it('defaults ops fork to user_in_chat', () => {
|
||||||
|
assert.equal(DEFAULT_GROUP_SESSION_SCOPE, 'user_in_chat');
|
||||||
|
assert.equal(normalizeGroupSessionScope(undefined), 'user_in_chat');
|
||||||
|
assert.equal(normalizeGroupSessionScope('shared'), 'chat');
|
||||||
|
assert.equal(normalizeGroupSessionScope('per_user'), 'user_in_chat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates strict scopes only', () => {
|
||||||
|
assert.equal(validateGroupSessionScope('chat'), 'chat');
|
||||||
|
assert.equal(validateGroupSessionScope('user_in_chat'), 'user_in_chat');
|
||||||
|
assert.throws(() => validateGroupSessionScope('shared'), /Invalid group session scope/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds direct and group conversation keys', () => {
|
||||||
|
assert.equal(
|
||||||
|
conversationKeyFor({ kind: 'direct', conversationId: 'u1', senderId: 'u1' }),
|
||||||
|
'direct:u1',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
conversationKeyFor({
|
||||||
|
kind: 'group',
|
||||||
|
conversationId: 'g1',
|
||||||
|
senderId: 'u2',
|
||||||
|
groupSessionScope: 'chat',
|
||||||
|
}),
|
||||||
|
'group:g1',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
conversationKeyFor({
|
||||||
|
kind: 'group',
|
||||||
|
conversationId: 'g1',
|
||||||
|
senderId: 'u2',
|
||||||
|
groupSessionScope: 'user_in_chat',
|
||||||
|
}),
|
||||||
|
'group:g1:user:u2',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires senderId for user_in_chat groups', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => conversationKeyFor({
|
||||||
|
kind: 'group',
|
||||||
|
conversationId: 'g1',
|
||||||
|
senderId: '',
|
||||||
|
groupSessionScope: 'user_in_chat',
|
||||||
|
}),
|
||||||
|
/senderId is required/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue