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:
oliver 2026-09-05 15:39:28 +08:00
parent d10a941fc8
commit 7f1b9bd480
30 changed files with 798 additions and 282 deletions

View file

@ -571,7 +571,7 @@ var React23 = __toESM(require("react"), 1);
// package.json
var package_default = {
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.",
keywords: [
"deepseek-harness",
@ -11797,7 +11797,29 @@ var React21 = __toESM(require("react"), 1);
// plugin-src/client/access-policy-settings.js
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 GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
weixin: Object.freeze({
directUserLabel: "\u5FAE\u4FE1\u7528\u6237 ID",
@ -12092,15 +12114,18 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
const definition = ACCESS_CHANNEL_DEFINITIONS[channel4];
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
const initialKey = JSON.stringify(initialPolicy);
const initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
const [draft, setDraft] = React20.useState(() => clonePolicy(
initialPolicy ?? DEFAULT_ACCESS_POLICY
));
const [groupSessionScope, setGroupSessionScope] = React20.useState(initialScope);
const [saving, setSaving] = React20.useState(false);
const [feedback, setFeedback] = React20.useState(null);
React20.useEffect(() => {
const next = normalizeAccessPolicy(account?.accessPolicy);
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
}, [account?.botId, initialKey]);
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
}, [account?.botId, initialKey, account?.groupSessionScope]);
React20.useEffect(() => {
setFeedback(null);
}, [account?.botId]);
@ -12117,16 +12142,25 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
setSaving(true);
try {
const policy = validateAccessPolicy(draft);
const scope = validateGroupSessionScope(groupSessionScope);
if (typeof rpcCall !== "function") throw new Error("\u8BBF\u95EE\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
const value = unwrapRpcResult10(await rpcCall(ACCESS_POLICY_ENDPOINT, {
botId: account.botId,
policy
}));
const scopeValue = unwrapRpcResult10(await rpcCall(GROUP_SESSION_SCOPE_ENDPOINT, {
botId: account.botId,
groupSessionScope: scope
}));
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");
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));
onSaved?.(saved);
setFeedback({ tone: "success", message: "\u8BBF\u95EE\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002" });
setGroupSessionScope(savedScope);
onSaved?.({ ...saved, groupSessionScope: savedScope });
setFeedback({ tone: "success", message: "\u8BBF\u95EE\u4E0E\u7FA4\u4F1A\u8BDD\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002" });
} catch (error) {
setFeedback({
tone: "error",
@ -12172,6 +12206,59 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
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", {
className: "dim-accessFeedback",
"data-tone": feedback.tone,
@ -12186,7 +12273,7 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
className: "dim-deliveryButton",
"data-kind": "primary",
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")
)
);
}

File diff suppressed because one or more lines are too long