mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-18 07:23:17 +08:00
Keep IM tool approvals in chat and add WhatsApp default model.
Claim in-flight IM turns before ownership flips active so approvals no longer stick only in DSH Web, auto-recover stale session models once, and let WhatsApp bots pick a shared default model for new sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2723e94541
commit
74c58bdcde
21 changed files with 1388 additions and 395 deletions
457
lib/client.js
457
lib/client.js
|
|
@ -566,7 +566,7 @@ __export(index_exports, {
|
||||||
name: () => name
|
name: () => name
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(index_exports);
|
module.exports = __toCommonJS(index_exports);
|
||||||
var React25 = __toESM(require("react"), 1);
|
var React26 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
|
|
@ -11122,6 +11122,177 @@ function installWeixinStyles() {
|
||||||
return () => style.remove();
|
return () => style.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plugin-src/client/default-model.js
|
||||||
|
var React19 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
|
// src/channels/shared/default-model.mjs
|
||||||
|
function normalizeDefaultModel(value) {
|
||||||
|
if (value == null || value === "") return null;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const slash = trimmed.indexOf("/");
|
||||||
|
if (slash <= 0 || slash === trimmed.length - 1) return null;
|
||||||
|
return {
|
||||||
|
provider: trimmed.slice(0, slash),
|
||||||
|
model: trimmed.slice(slash + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||||
|
const provider = typeof value.provider === "string" ? value.provider.trim() : "";
|
||||||
|
const model = typeof value.model === "string" ? value.model.trim() : "";
|
||||||
|
if (!provider || !model) return null;
|
||||||
|
const selection = { provider, model };
|
||||||
|
if (value.reasoningEffort !== void 0) {
|
||||||
|
if (typeof value.reasoningEffort !== "string" || !value.reasoningEffort.trim()) return null;
|
||||||
|
selection.reasoningEffort = value.reasoningEffort.trim();
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
function defaultModelKey(selection) {
|
||||||
|
const normalized = normalizeDefaultModel(selection);
|
||||||
|
return normalized ? `${normalized.provider}/${normalized.model}` : "";
|
||||||
|
}
|
||||||
|
function modelCatalogItems(catalog) {
|
||||||
|
const items = [];
|
||||||
|
const seen = /* @__PURE__ */ new Set();
|
||||||
|
for (const group of Array.isArray(catalog?.groups) ? catalog.groups : []) {
|
||||||
|
const provider = typeof group?.id === "string" ? group.id.trim() : "";
|
||||||
|
if (!provider || !Array.isArray(group.models)) continue;
|
||||||
|
for (const model of group.models) {
|
||||||
|
const modelId = typeof model?.id === "string" ? model.id.trim() : "";
|
||||||
|
if (!modelId) continue;
|
||||||
|
const key = `${provider}/${modelId}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
const label = typeof model.name === "string" && model.name.trim() ? model.name.trim() : modelId;
|
||||||
|
items.push({
|
||||||
|
provider,
|
||||||
|
model: modelId,
|
||||||
|
key,
|
||||||
|
label: label === modelId ? key : `${label}\uFF08${key}\uFF09`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
function normalizeModelCatalog(value) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return { groups: [], failures: [], items: [] };
|
||||||
|
}
|
||||||
|
const groups = Array.isArray(value.groups) ? value.groups : [];
|
||||||
|
const failures = Array.isArray(value.failures) ? value.failures : [];
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
failures,
|
||||||
|
items: modelCatalogItems({ groups, failures })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugin-src/client/default-model.js
|
||||||
|
var SET_DEFAULT_MODEL_ENDPOINT = "bot.model.set";
|
||||||
|
var EMPTY_MODEL_CATALOG = Object.freeze({
|
||||||
|
groups: Object.freeze([]),
|
||||||
|
failures: Object.freeze([]),
|
||||||
|
items: Object.freeze([])
|
||||||
|
});
|
||||||
|
var ModelCatalogContext = React19.createContext(EMPTY_MODEL_CATALOG);
|
||||||
|
function normalizeDefaultModelSelection(value) {
|
||||||
|
return normalizeDefaultModel(value);
|
||||||
|
}
|
||||||
|
function normalizeHostModelCatalog(value) {
|
||||||
|
return normalizeModelCatalog(value);
|
||||||
|
}
|
||||||
|
function DefaultModelEditor({ defaultModel = null, disabled = false, onSave }) {
|
||||||
|
const catalog = React19.useContext(ModelCatalogContext) ?? EMPTY_MODEL_CATALOG;
|
||||||
|
const helpId = React19.useId();
|
||||||
|
const current = normalizeDefaultModel(defaultModel);
|
||||||
|
const currentKey = defaultModelKey(current);
|
||||||
|
const [saving, setSaving] = React19.useState(false);
|
||||||
|
const [error, setError] = React19.useState(null);
|
||||||
|
const items = [];
|
||||||
|
const seen = /* @__PURE__ */ new Set();
|
||||||
|
for (const item of Array.isArray(catalog.items) ? catalog.items : []) {
|
||||||
|
if (!item?.key || seen.has(item.key)) continue;
|
||||||
|
seen.add(item.key);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
const currentUnavailable = Boolean(currentKey && !seen.has(currentKey));
|
||||||
|
if (currentUnavailable && current) {
|
||||||
|
items.push({
|
||||||
|
key: currentKey,
|
||||||
|
provider: current.provider,
|
||||||
|
model: current.model,
|
||||||
|
label: currentKey,
|
||||||
|
unavailable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const change = async (event) => {
|
||||||
|
const nextKey = event.target.value;
|
||||||
|
if (nextKey === currentKey || saving || disabled) return;
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await onSave?.(nextKey ? normalizeDefaultModel(nextKey) : null);
|
||||||
|
} catch (cause) {
|
||||||
|
setError(cause?.message ?? "\u9ED8\u8BA4\u6A21\u578B\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return h2(
|
||||||
|
"div",
|
||||||
|
{ className: "dim-preset" },
|
||||||
|
h2(
|
||||||
|
"div",
|
||||||
|
{ className: "dim-presetHeader" },
|
||||||
|
h2(
|
||||||
|
"span",
|
||||||
|
{ className: "dim-presetTitle" },
|
||||||
|
h2("span", null, "\u9ED8\u8BA4\u6A21\u578B"),
|
||||||
|
h2(
|
||||||
|
"span",
|
||||||
|
{ className: "dim-presetHelp" },
|
||||||
|
h2("button", {
|
||||||
|
type: "button",
|
||||||
|
className: "dim-presetHelpButton",
|
||||||
|
"aria-label": "\u67E5\u770B\u9ED8\u8BA4\u6A21\u578B\u8BF4\u660E",
|
||||||
|
"aria-describedby": helpId
|
||||||
|
}, h2("span", { "aria-hidden": "true" }, "?")),
|
||||||
|
h2("span", {
|
||||||
|
id: helpId,
|
||||||
|
className: "dim-presetTooltip",
|
||||||
|
role: "tooltip"
|
||||||
|
}, "\u4E3A\u8BE5 WhatsApp \u673A\u5668\u4EBA\u7684\u65B0\u5EFA\u4F1A\u8BDD\u6307\u5B9A\u6A21\u578B\uFF0C\u907F\u514D\u6BCF\u4EBA\u5404\u81EA /model\u3002\u5DF2\u6709\u4F1A\u8BDD\u9700\u5148 /new \u518D\u53D1\u6D88\u606F\u624D\u4F1A\u751F\u6548\u3002")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
saving ? h2("span", { className: "dim-presetStatus" }, "\u4FDD\u5B58\u4E2D\u2026") : null
|
||||||
|
),
|
||||||
|
React19.createElement(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
className: "dim-presetSelect",
|
||||||
|
value: currentKey,
|
||||||
|
disabled: disabled || saving,
|
||||||
|
"aria-label": "\u9ED8\u8BA4\u6A21\u578B",
|
||||||
|
onChange: (event) => {
|
||||||
|
void change(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h2("option", { value: "" }, "\u8DDF\u968F Host \u9ED8\u8BA4"),
|
||||||
|
...items.map((item) => h2(
|
||||||
|
"option",
|
||||||
|
{ key: item.key, value: item.key },
|
||||||
|
item.unavailable ? [item.key, "\uFF08\u5DF2\u4E0D\u53EF\u7528\uFF09"] : item.label
|
||||||
|
))
|
||||||
|
),
|
||||||
|
error || currentUnavailable ? h2(
|
||||||
|
"p",
|
||||||
|
{ className: "dim-presetError", role: error ? "alert" : "status" },
|
||||||
|
error ?? "\u5F53\u524D\u9ED8\u8BA4\u6A21\u578B\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u6A21\u578B\u6216\u8DDF\u968F Host \u9ED8\u8BA4\u3002"
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// src/channels/shared/access-grant.mjs
|
// src/channels/shared/access-grant.mjs
|
||||||
var ACCESS_GRANT_VERSION = 1;
|
var ACCESS_GRANT_VERSION = 1;
|
||||||
var ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
var ACCESS_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
||||||
|
|
@ -11359,6 +11530,7 @@ var WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
setGroupSessionScope: "bot.group-session-scope.set",
|
setGroupSessionScope: "bot.group-session-scope.set",
|
||||||
setWorkspace: "bot.workspace.set",
|
setWorkspace: "bot.workspace.set",
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
|
setDefaultModel: SET_DEFAULT_MODEL_ENDPOINT,
|
||||||
setContextEnhancement: "bot.context-enhancement.set",
|
setContextEnhancement: "bot.context-enhancement.set",
|
||||||
resolveChannelPeer: "bot.session.channel-peer"
|
resolveChannelPeer: "bot.session.channel-peer"
|
||||||
});
|
});
|
||||||
|
|
@ -11426,6 +11598,7 @@ function normalizeBot6(value) {
|
||||||
state: connected ? "connected" : state,
|
state: connected ? "connected" : state,
|
||||||
workspace: text5(value.workspace, "", 4096),
|
workspace: text5(value.workspace, "", 4096),
|
||||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||||
|
defaultModel: normalizeDefaultModelSelection(value.defaultModel),
|
||||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||||
...Object.hasOwn(value, "accessPolicy") ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) } : {},
|
...Object.hasOwn(value, "accessPolicy") ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) } : {},
|
||||||
...Object.hasOwn(value, "accessGrant") ? { accessGrant: normalizeAccessGrant(value.accessGrant) } : {},
|
...Object.hasOwn(value, "accessGrant") ? { accessGrant: normalizeAccessGrant(value.accessGrant) } : {},
|
||||||
|
|
@ -11456,7 +11629,8 @@ function normalizeSnapshot8(value) {
|
||||||
bots,
|
bots,
|
||||||
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
||||||
provisioning: source.provisioning ? normalizeProvisioning6(source.provisioning) : null,
|
provisioning: source.provisioning ? normalizeProvisioning6(source.provisioning) : null,
|
||||||
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog)
|
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
|
||||||
|
modelCatalog: normalizeHostModelCatalog(source.modelCatalog)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function presentError9(error) {
|
function presentError9(error) {
|
||||||
|
|
@ -11471,7 +11645,7 @@ function formatRemaining6(milliseconds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// plugin-src/client/channels/whatsapp/index.js
|
// plugin-src/client/channels/whatsapp/index.js
|
||||||
var React19 = __toESM(require("react"), 1);
|
var React20 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
// plugin-src/client/channels/whatsapp/styles.js
|
// plugin-src/client/channels/whatsapp/styles.js
|
||||||
var WHATSAPP_STYLE_ID = "xmanrui-dsh-im-whatsapp-settings";
|
var WHATSAPP_STYLE_ID = "xmanrui-dsh-im-whatsapp-settings";
|
||||||
|
|
@ -11496,7 +11670,7 @@ function installWhatsappStyles() {
|
||||||
|
|
||||||
// plugin-src/client/channels/whatsapp/index.js
|
// plugin-src/client/channels/whatsapp/index.js
|
||||||
var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
|
var ACTIVE_STATES3 = /* @__PURE__ */ new Set(["pending", "connecting"]);
|
||||||
var Button14 = React19.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) {
|
var Button14 = React20.forwardRef(function Button15({ children, kind = "secondary", className = "", ...props }, ref) {
|
||||||
return h2("button", {
|
return h2("button", {
|
||||||
...props,
|
...props,
|
||||||
ref,
|
ref,
|
||||||
|
|
@ -11719,6 +11893,7 @@ function WhatsappAccountCard({
|
||||||
onReconnect,
|
onReconnect,
|
||||||
onWorkspaceSave,
|
onWorkspaceSave,
|
||||||
onAgentPresetSave,
|
onAgentPresetSave,
|
||||||
|
onDefaultModelSave,
|
||||||
onContextEnhancementSave,
|
onContextEnhancementSave,
|
||||||
onRequestRemove,
|
onRequestRemove,
|
||||||
onConfirmRemove,
|
onConfirmRemove,
|
||||||
|
|
@ -11783,6 +11958,11 @@ function WhatsappAccountCard({
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
onSave: onAgentPresetSave
|
onSave: onAgentPresetSave
|
||||||
}),
|
}),
|
||||||
|
h2(DefaultModelEditor, {
|
||||||
|
defaultModel: account.defaultModel,
|
||||||
|
disabled: Boolean(busy),
|
||||||
|
onSave: onDefaultModelSave
|
||||||
|
}),
|
||||||
h2(ContextEnhancementEditor, {
|
h2(ContextEnhancementEditor, {
|
||||||
config: account.contextEnhancement,
|
config: account.contextEnhancement,
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
|
|
@ -11830,23 +12010,24 @@ function WhatsappAccountCard({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function WhatsappSettingsTab({ rpcCall }) {
|
function WhatsappSettingsTab({ rpcCall }) {
|
||||||
const [model, setModel] = React19.useState({
|
const [model, setModel] = React20.useState({
|
||||||
phase: "loading",
|
phase: "loading",
|
||||||
bots: [],
|
bots: [],
|
||||||
totals: { configured: 0, connected: 0 },
|
totals: { configured: 0, connected: 0 },
|
||||||
error: null,
|
error: null,
|
||||||
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG
|
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: EMPTY_MODEL_CATALOG
|
||||||
});
|
});
|
||||||
const [provision, setProvision] = React19.useState(null);
|
const [provision, setProvision] = React20.useState(null);
|
||||||
const [busy, setBusy] = React19.useState(false);
|
const [busy, setBusy] = React20.useState(false);
|
||||||
const [busyByBot, setBusyByBot] = React19.useState({});
|
const [busyByBot, setBusyByBot] = React20.useState({});
|
||||||
const [testNoticeByBot, setTestNoticeByBot] = React19.useState({});
|
const [testNoticeByBot, setTestNoticeByBot] = React20.useState({});
|
||||||
const [removeTarget, setRemoveTarget] = React19.useState(null);
|
const [removeTarget, setRemoveTarget] = React20.useState(null);
|
||||||
const [now, setNow] = React19.useState(Date.now());
|
const [now, setNow] = React20.useState(Date.now());
|
||||||
const mounted = React19.useRef(true);
|
const mounted = React20.useRef(true);
|
||||||
const workspaceFence = useWorkspaceSnapshotFence();
|
const workspaceFence = useWorkspaceSnapshotFence();
|
||||||
const addButtonRef = React19.useRef(null);
|
const addButtonRef = React20.useRef(null);
|
||||||
React19.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
const disposeDingtalk = installDingtalkStyles();
|
const disposeDingtalk = installDingtalkStyles();
|
||||||
const disposeWhatsapp = installWhatsappStyles();
|
const disposeWhatsapp = installWhatsappStyles();
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
|
|
@ -11856,11 +12037,11 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
disposeDingtalk();
|
disposeDingtalk();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const invoke = React19.useCallback(async (endpoint, payload = {}, signal) => {
|
const invoke = React20.useCallback(async (endpoint, payload = {}, signal) => {
|
||||||
if (typeof rpcCall !== "function") throw new TypeError("WhatsApp \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
|
if (typeof rpcCall !== "function") throw new TypeError("WhatsApp \u8BBE\u7F6E\u9875\u7F3A\u5C11 RPC \u8FDE\u63A5");
|
||||||
return unwrapRpcResult9(await rpcCall(endpoint, payload, signal));
|
return unwrapRpcResult9(await rpcCall(endpoint, payload, signal));
|
||||||
}, [rpcCall]);
|
}, [rpcCall]);
|
||||||
const loadStatus = React19.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
const loadStatus = React20.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
||||||
const workspaceVersion = workspaceFence.beginStatus();
|
const workspaceVersion = workspaceFence.beginStatus();
|
||||||
if (workspaceVersion === null) return void 0;
|
if (workspaceVersion === null) return void 0;
|
||||||
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
|
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: "loading", error: null }));
|
||||||
|
|
@ -11872,7 +12053,8 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
bots: snapshot.bots,
|
bots: snapshot.bots,
|
||||||
totals: snapshot.totals,
|
totals: snapshot.totals,
|
||||||
error: null,
|
error: null,
|
||||||
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG
|
||||||
});
|
});
|
||||||
if (restore && snapshot.provisioning) setProvision({
|
if (restore && snapshot.provisioning) setProvision({
|
||||||
...snapshot.provisioning,
|
...snapshot.provisioning,
|
||||||
|
|
@ -11890,12 +12072,12 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
}, [invoke, workspaceFence]);
|
}, [invoke, workspaceFence]);
|
||||||
React19.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
void loadStatus({ signal: controller.signal, restore: true });
|
void loadStatus({ signal: controller.signal, restore: true });
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [loadStatus]);
|
}, [loadStatus]);
|
||||||
React19.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
if (model.phase !== "ready") return void 0;
|
if (model.phase !== "ready") return void 0;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = window.setInterval(
|
const timer = window.setInterval(
|
||||||
|
|
@ -11907,12 +12089,12 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
window.clearInterval(timer);
|
window.clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, [loadStatus, model.phase]);
|
}, [loadStatus, model.phase]);
|
||||||
React19.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
if (!provision || !ACTIVE_STATES3.has(provision.status)) return void 0;
|
if (!provision || !ACTIVE_STATES3.has(provision.status)) return void 0;
|
||||||
const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
|
const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1e3);
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [provision?.attemptId, provision?.status]);
|
}, [provision?.attemptId, provision?.status]);
|
||||||
const startProvisioning = React19.useCallback(async (replace = false) => {
|
const startProvisioning = React20.useCallback(async (replace = false) => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
if (replace && provision?.attemptId) {
|
if (replace && provision?.attemptId) {
|
||||||
|
|
@ -11930,7 +12112,7 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
if (mounted.current) setBusy(false);
|
if (mounted.current) setBusy(false);
|
||||||
}
|
}
|
||||||
}, [invoke, provision?.attemptId]);
|
}, [invoke, provision?.attemptId]);
|
||||||
const closeProvision = React19.useCallback(async () => {
|
const closeProvision = React20.useCallback(async () => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
if (provision?.attemptId && ACTIVE_STATES3.has(provision.status)) {
|
if (provision?.attemptId && ACTIVE_STATES3.has(provision.status)) {
|
||||||
|
|
@ -11941,7 +12123,7 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
if (mounted.current) setBusy(false);
|
if (mounted.current) setBusy(false);
|
||||||
}
|
}
|
||||||
}, [invoke, provision?.attemptId, provision?.status]);
|
}, [invoke, provision?.attemptId, provision?.status]);
|
||||||
React19.useEffect(() => {
|
React20.useEffect(() => {
|
||||||
const attemptId = provision?.attemptId;
|
const attemptId = provision?.attemptId;
|
||||||
if (!attemptId || !ACTIVE_STATES3.has(provision.status)) return void 0;
|
if (!attemptId || !ACTIVE_STATES3.has(provision.status)) return void 0;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -11982,7 +12164,7 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
if (timer) window.clearTimeout(timer);
|
if (timer) window.clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [invoke, loadStatus, provision?.attemptId, provision?.status]);
|
}, [invoke, loadStatus, provision?.attemptId, provision?.status]);
|
||||||
const botAction = React19.useCallback(async (account, operation, endpoint, payload) => {
|
const botAction = React20.useCallback(async (account, operation, endpoint, payload) => {
|
||||||
const snapshotVersion = workspaceFence.beginMutation();
|
const snapshotVersion = workspaceFence.beginMutation();
|
||||||
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
||||||
if (operation === "reconnect") {
|
if (operation === "reconnect") {
|
||||||
|
|
@ -12001,7 +12183,8 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
bots: snapshot.bots,
|
bots: snapshot.bots,
|
||||||
totals: snapshot.totals,
|
totals: snapshot.totals,
|
||||||
error: null,
|
error: null,
|
||||||
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG
|
||||||
});
|
});
|
||||||
if (operation === "reconnect") {
|
if (operation === "reconnect") {
|
||||||
setTestNoticeByBot((current) => ({
|
setTestNoticeByBot((current) => ({
|
||||||
|
|
@ -12059,6 +12242,12 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
WHATSAPP_ENDPOINTS.setAgentPreset,
|
WHATSAPP_ENDPOINTS.setAgentPreset,
|
||||||
{ botId: account.botId, agentPreset }
|
{ botId: account.botId, agentPreset }
|
||||||
),
|
),
|
||||||
|
onDefaultModelSave: (defaultModel) => botAction(
|
||||||
|
account,
|
||||||
|
"model",
|
||||||
|
WHATSAPP_ENDPOINTS.setDefaultModel,
|
||||||
|
{ botId: account.botId, defaultModel }
|
||||||
|
),
|
||||||
onContextEnhancementSave: (config) => botAction(
|
onContextEnhancementSave: (config) => botAction(
|
||||||
account,
|
account,
|
||||||
"context-enhancement",
|
"context-enhancement",
|
||||||
|
|
@ -12078,6 +12267,8 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
) : null;
|
) : null;
|
||||||
return h2(AgentPresetCatalogContext.Provider, {
|
return h2(AgentPresetCatalogContext.Provider, {
|
||||||
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
||||||
|
}, h2(ModelCatalogContext.Provider, {
|
||||||
|
value: model.modelCatalog ?? EMPTY_MODEL_CATALOG
|
||||||
}, h2(
|
}, h2(
|
||||||
"section",
|
"section",
|
||||||
{
|
{
|
||||||
|
|
@ -12101,7 +12292,7 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
h2(Button14, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
|
h2(Button14, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6")
|
||||||
)
|
)
|
||||||
) : h2(
|
) : h2(
|
||||||
React19.Fragment,
|
React20.Fragment,
|
||||||
null,
|
null,
|
||||||
provision?.status === "pending" ? h2(QrPanel5, {
|
provision?.status === "pending" ? h2(QrPanel5, {
|
||||||
provision,
|
provision,
|
||||||
|
|
@ -12117,11 +12308,11 @@ function WhatsappSettingsTab({ rpcCall }) {
|
||||||
}) : model.bots.length === 0 ? h2(EmptyView6, { busy, onStart: () => void startProvisioning(false) }) : null,
|
}) : model.bots.length === 0 ? h2(EmptyView6, { busy, onStart: () => void startProvisioning(false) }) : null,
|
||||||
botList
|
botList
|
||||||
)
|
)
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// plugin-src/client/channel-peer-label.js
|
// plugin-src/client/channel-peer-label.js
|
||||||
var React20 = __toESM(require("react"), 1);
|
var React21 = __toESM(require("react"), 1);
|
||||||
var CHANNEL_LABELS = Object.freeze({
|
var CHANNEL_LABELS = Object.freeze({
|
||||||
whatsapp: "WhatsApp",
|
whatsapp: "WhatsApp",
|
||||||
feishu: "\u98DE\u4E66",
|
feishu: "\u98DE\u4E66",
|
||||||
|
|
@ -12135,8 +12326,8 @@ var CHANNEL_LABELS = Object.freeze({
|
||||||
office: "AI Office"
|
office: "AI Office"
|
||||||
});
|
});
|
||||||
function ChannelPeerLabel({ sessionId, resolveChannelPeer }) {
|
function ChannelPeerLabel({ sessionId, resolveChannelPeer }) {
|
||||||
const [peer, setPeer] = React20.useState(null);
|
const [peer, setPeer] = React21.useState(null);
|
||||||
React20.useEffect(() => {
|
React21.useEffect(() => {
|
||||||
if (typeof resolveChannelPeer !== "function" || typeof sessionId !== "string" || !sessionId) {
|
if (typeof resolveChannelPeer !== "function" || typeof sessionId !== "string" || !sessionId) {
|
||||||
setPeer(null);
|
setPeer(null);
|
||||||
return void 0;
|
return void 0;
|
||||||
|
|
@ -12193,10 +12384,10 @@ async function resolveWhatsappChannelPeer(whatsappRpcCall, sessionId, signal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// plugin-src/client/delivery-settings.js
|
// plugin-src/client/delivery-settings.js
|
||||||
var React23 = __toESM(require("react"), 1);
|
var React24 = __toESM(require("react"), 1);
|
||||||
|
|
||||||
// plugin-src/client/access-policy-settings.js
|
// plugin-src/client/access-policy-settings.js
|
||||||
var React21 = __toESM(require("react"), 1);
|
var React22 = __toESM(require("react"), 1);
|
||||||
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 GROUP_SESSION_SCOPE_ENDPOINT = "bot.group-session-scope.set";
|
||||||
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
var ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||||
|
|
@ -12299,8 +12490,8 @@ function ScenePolicyEditor({
|
||||||
unsupported = false,
|
unsupported = false,
|
||||||
onChange
|
onChange
|
||||||
}) {
|
}) {
|
||||||
const ownerHelpId = React21.useId();
|
const ownerHelpId = React22.useId();
|
||||||
const emptyAllowlistHelpId = React21.useId();
|
const emptyAllowlistHelpId = React22.useId();
|
||||||
const allowlist = policy.mode === "allowlist";
|
const allowlist = policy.mode === "allowlist";
|
||||||
const collectionKey = allowlist ? "users" : "commandPermissionOverrides";
|
const collectionKey = allowlist ? "users" : "commandPermissionOverrides";
|
||||||
const branchKey = allowlist ? "allowlist" : "open";
|
const branchKey = allowlist ? "allowlist" : "open";
|
||||||
|
|
@ -12352,7 +12543,7 @@ function ScenePolicyEditor({
|
||||||
h2("strong", null, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u7FA4\u804A"),
|
h2("strong", null, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u7FA4\u804A"),
|
||||||
h2("p", null, "\u6B64\u533A\u57DF\u65E0\u9700\u914D\u7F6E\uFF0C\u4FDD\u5B58\u79C1\u804A\u8BBE\u7F6E\u65F6\u4F1A\u4FDD\u7559\u73B0\u6709\u7FA4\u804A\u7B56\u7565\u3002")
|
h2("p", null, "\u6B64\u533A\u57DF\u65E0\u9700\u914D\u7F6E\uFF0C\u4FDD\u5B58\u79C1\u804A\u8BBE\u7F6E\u65F6\u4F1A\u4FDD\u7559\u73B0\u6709\u7FA4\u804A\u7B56\u7565\u3002")
|
||||||
) : h2(
|
) : h2(
|
||||||
React21.Fragment,
|
React22.Fragment,
|
||||||
null,
|
null,
|
||||||
h2(
|
h2(
|
||||||
"div",
|
"div",
|
||||||
|
|
@ -12494,18 +12685,18 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
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 initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
|
||||||
const [draft, setDraft] = React21.useState(() => clonePolicy(
|
const [draft, setDraft] = React22.useState(() => clonePolicy(
|
||||||
initialPolicy ?? DEFAULT_ACCESS_POLICY
|
initialPolicy ?? DEFAULT_ACCESS_POLICY
|
||||||
));
|
));
|
||||||
const [groupSessionScope, setGroupSessionScope] = React21.useState(initialScope);
|
const [groupSessionScope, setGroupSessionScope] = React22.useState(initialScope);
|
||||||
const [saving, setSaving] = React21.useState(false);
|
const [saving, setSaving] = React22.useState(false);
|
||||||
const [feedback, setFeedback] = React21.useState(null);
|
const [feedback, setFeedback] = React22.useState(null);
|
||||||
React21.useEffect(() => {
|
React22.useEffect(() => {
|
||||||
const next = normalizeAccessPolicy(account?.accessPolicy);
|
const next = normalizeAccessPolicy(account?.accessPolicy);
|
||||||
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
||||||
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
||||||
}, [account?.botId, initialKey, account?.groupSessionScope]);
|
}, [account?.botId, initialKey, account?.groupSessionScope]);
|
||||||
React21.useEffect(() => {
|
React22.useEffect(() => {
|
||||||
setFeedback(null);
|
setFeedback(null);
|
||||||
}, [account?.botId]);
|
}, [account?.botId]);
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
|
|
@ -12658,7 +12849,7 @@ function AccessPolicySettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
}
|
}
|
||||||
|
|
||||||
// plugin-src/client/access-grant-settings.js
|
// plugin-src/client/access-grant-settings.js
|
||||||
var React22 = __toESM(require("react"), 1);
|
var React23 = __toESM(require("react"), 1);
|
||||||
var ACCESS_GRANT_ENDPOINT = "bot.access-grant.set";
|
var ACCESS_GRANT_ENDPOINT = "bot.access-grant.set";
|
||||||
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
var ACCESS_PENDING_RESOLVE_ENDPOINT = "bot.access-pending.resolve";
|
||||||
var ACCESS_GROUP_TITLES_REFRESH_ENDPOINT = "bot.access-group-titles.refresh";
|
var ACCESS_GROUP_TITLES_REFRESH_ENDPOINT = "bot.access-group-titles.refresh";
|
||||||
|
|
@ -12750,13 +12941,13 @@ function PhoneTypeahead({
|
||||||
disabled,
|
disabled,
|
||||||
requirePhone = true
|
requirePhone = true
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = React22.useState(value || "");
|
const [query, setQuery] = React23.useState(value || "");
|
||||||
const [open, setOpen] = React22.useState(false);
|
const [open, setOpen] = React23.useState(false);
|
||||||
const blurTimer = React22.useRef(null);
|
const blurTimer = React23.useRef(null);
|
||||||
React22.useEffect(() => {
|
React23.useEffect(() => {
|
||||||
setQuery(value || "");
|
setQuery(value || "");
|
||||||
}, [value]);
|
}, [value]);
|
||||||
React22.useEffect(() => () => {
|
React23.useEffect(() => () => {
|
||||||
if (blurTimer.current) window.clearTimeout(blurTimer.current);
|
if (blurTimer.current) window.clearTimeout(blurTimer.current);
|
||||||
}, []);
|
}, []);
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
|
|
@ -13064,21 +13255,21 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
const initialGrant = normalizeAccessGrant(account?.accessGrant);
|
const initialGrant = normalizeAccessGrant(account?.accessGrant);
|
||||||
const initialKey = JSON.stringify(initialGrant);
|
const initialKey = JSON.stringify(initialGrant);
|
||||||
const initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
|
const initialScope = normalizeGroupSessionScope(account?.groupSessionScope);
|
||||||
const [draft, setDraft] = React22.useState(() => cloneGrant(initialGrant, ownerPhone));
|
const [draft, setDraft] = React23.useState(() => cloneGrant(initialGrant, ownerPhone));
|
||||||
const [groupSessionScope, setGroupSessionScope] = React22.useState(initialScope);
|
const [groupSessionScope, setGroupSessionScope] = React23.useState(initialScope);
|
||||||
const [saving, setSaving] = React22.useState(false);
|
const [saving, setSaving] = React23.useState(false);
|
||||||
const [feedback, setFeedback] = React22.useState(null);
|
const [feedback, setFeedback] = React23.useState(null);
|
||||||
const [newGroupJid, setNewGroupJid] = React22.useState("");
|
const [newGroupJid, setNewGroupJid] = React23.useState("");
|
||||||
const [loadingGrant, setLoadingGrant] = React22.useState(!initialGrant);
|
const [loadingGrant, setLoadingGrant] = React23.useState(!initialGrant);
|
||||||
const contextCatalog = React22.useContext(AgentPresetCatalogContext);
|
const contextCatalog = React23.useContext(AgentPresetCatalogContext);
|
||||||
const [presetCatalog, setPresetCatalog] = React22.useState(() => normalizeAgentPresetCatalog(
|
const [presetCatalog, setPresetCatalog] = React23.useState(() => normalizeAgentPresetCatalog(
|
||||||
account?.agentPresetCatalog ?? contextCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
account?.agentPresetCatalog ?? contextCatalog ?? EMPTY_AGENT_PRESET_CATALOG
|
||||||
));
|
));
|
||||||
React22.useEffect(() => {
|
React23.useEffect(() => {
|
||||||
setDraft(cloneGrant(normalizeAccessGrant(account?.accessGrant), ownerPhone));
|
setDraft(cloneGrant(normalizeAccessGrant(account?.accessGrant), ownerPhone));
|
||||||
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
setGroupSessionScope(normalizeGroupSessionScope(account?.groupSessionScope));
|
||||||
}, [account?.botId, initialKey, account?.groupSessionScope, ownerPhone]);
|
}, [account?.botId, initialKey, account?.groupSessionScope, ownerPhone]);
|
||||||
React22.useEffect(() => {
|
React23.useEffect(() => {
|
||||||
if (typeof rpcCall !== "function" || !account?.botId) return void 0;
|
if (typeof rpcCall !== "function" || !account?.botId) return void 0;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoadingGrant(true);
|
setLoadingGrant(true);
|
||||||
|
|
@ -13132,7 +13323,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
};
|
};
|
||||||
}, [account?.botId, rpcCall, ownerPhone]);
|
}, [account?.botId, rpcCall, ownerPhone]);
|
||||||
const contacts = draft.contacts ?? [];
|
const contacts = draft.contacts ?? [];
|
||||||
const knownGroupJids = React22.useMemo(() => {
|
const knownGroupJids = React23.useMemo(() => {
|
||||||
const set = new Set(Object.keys(draft.groups ?? {}));
|
const set = new Set(Object.keys(draft.groups ?? {}));
|
||||||
for (const contact of contacts) {
|
for (const contact of contacts) {
|
||||||
for (const jid of contact.groupJids ?? []) set.add(jid);
|
for (const jid of contact.groupJids ?? []) set.add(jid);
|
||||||
|
|
@ -13142,7 +13333,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
|
||||||
}
|
}
|
||||||
return [...set].sort();
|
return [...set].sort();
|
||||||
}, [draft.groups, draft.pending, contacts]);
|
}, [draft.groups, draft.pending, contacts]);
|
||||||
const actionableContacts = React22.useMemo(
|
const actionableContacts = React23.useMemo(
|
||||||
() => contacts.filter((contact) => contactNeedsGrantAction(contact, draft, knownGroupJids)),
|
() => contacts.filter((contact) => contactNeedsGrantAction(contact, draft, knownGroupJids)),
|
||||||
[contacts, draft, knownGroupJids]
|
[contacts, draft, knownGroupJids]
|
||||||
);
|
);
|
||||||
|
|
@ -13821,13 +14012,13 @@ function TargetForm({
|
||||||
}) {
|
}) {
|
||||||
const editing = mode === "edit";
|
const editing = mode === "edit";
|
||||||
const initialKind = initialValue?.kind && definition.fields[initialValue.kind] ? initialValue.kind : definition.kinds[0].value;
|
const initialKind = initialValue?.kind && definition.fields[initialValue.kind] ? initialValue.kind : definition.kinds[0].value;
|
||||||
const [targetId, setTargetId] = React23.useState(initialValue?.targetId ?? "");
|
const [targetId, setTargetId] = React24.useState(initialValue?.targetId ?? "");
|
||||||
const [name2, setName] = React23.useState(initialValue?.name ?? "");
|
const [name2, setName] = React24.useState(initialValue?.name ?? "");
|
||||||
const [kind, setKind] = React23.useState(initialKind);
|
const [kind, setKind] = React24.useState(initialKind);
|
||||||
const [route, setRoute] = React23.useState(initialValue?.route ?? {});
|
const [route, setRoute] = React24.useState(initialValue?.route ?? {});
|
||||||
const [error, setError] = React23.useState(null);
|
const [error, setError] = React24.useState(null);
|
||||||
const [testing, setTesting] = React23.useState(false);
|
const [testing, setTesting] = React24.useState(false);
|
||||||
const [testState, setTestState] = React23.useState(null);
|
const [testState, setTestState] = React24.useState(null);
|
||||||
const currentTarget = () => {
|
const currentTarget = () => {
|
||||||
const normalizedRoute = Object.fromEntries(fieldsFor(definition, kind).map((field) => {
|
const normalizedRoute = Object.fromEntries(fieldsFor(definition, kind).map((field) => {
|
||||||
const raw = String(route[field.key] ?? "").trim();
|
const raw = String(route[field.key] ?? "").trim();
|
||||||
|
|
@ -14042,7 +14233,7 @@ function TargetSuggestionPicker({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Platform display names must stay raw (bypass h() i18n phrase rewrite).
|
// Platform display names must stay raw (bypass h() i18n phrase rewrite).
|
||||||
React23.createElement(
|
React24.createElement(
|
||||||
"strong",
|
"strong",
|
||||||
null,
|
null,
|
||||||
suggestionDisplayName(suggestion) || suggestionFallbackName(definition, suggestion)
|
suggestionDisplayName(suggestion) || suggestionFallbackName(definition, suggestion)
|
||||||
|
|
@ -14051,7 +14242,7 @@ function TargetSuggestionPicker({
|
||||||
localizeText(kindLabel(definition, suggestion.kind)),
|
localizeText(kindLabel(definition, suggestion.kind)),
|
||||||
added ? localizeText("\u5DF2\u6DFB\u52A0") : null
|
added ? localizeText("\u5DF2\u6DFB\u52A0") : null
|
||||||
].filter(Boolean).join(" \xB7 ")),
|
].filter(Boolean).join(" \xB7 ")),
|
||||||
React23.createElement("code", null, fieldsFor(definition, suggestion.kind).map((field) => maskRouteValue(suggestion.route?.[field.key])).filter(Boolean).join(" \xB7 "))
|
React24.createElement("code", null, fieldsFor(definition, suggestion.kind).map((field) => maskRouteValue(suggestion.route?.[field.key])).filter(Boolean).join(" \xB7 "))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
@ -14065,10 +14256,10 @@ function TargetSuggestionPicker({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, onEdit }) {
|
function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, onEdit }) {
|
||||||
const [action, setAction] = React23.useState(null);
|
const [action, setAction] = React24.useState(null);
|
||||||
const [testState, setTestState] = React23.useState(null);
|
const [testState, setTestState] = React24.useState(null);
|
||||||
const [copyState, setCopyState] = React23.useState(null);
|
const [copyState, setCopyState] = React24.useState(null);
|
||||||
const [confirmDelete, setConfirmDelete] = React23.useState(false);
|
const [confirmDelete, setConfirmDelete] = React24.useState(false);
|
||||||
const testTarget = async () => {
|
const testTarget = async () => {
|
||||||
setAction("test");
|
setAction("test");
|
||||||
setTestState(null);
|
setTestState(null);
|
||||||
|
|
@ -14110,7 +14301,7 @@ function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, o
|
||||||
h2(
|
h2(
|
||||||
"div",
|
"div",
|
||||||
{ className: "dim-targetTitle" },
|
{ className: "dim-targetTitle" },
|
||||||
React23.createElement("strong", null, target.name || target.targetId),
|
React24.createElement("strong", null, target.name || target.targetId),
|
||||||
h2("span", null, kindLabel(definition, target.kind))
|
h2("span", null, kindLabel(definition, target.kind))
|
||||||
),
|
),
|
||||||
h2("code", null, `targetId: ${target.targetId}`)
|
h2("code", null, `targetId: ${target.targetId}`)
|
||||||
|
|
@ -14173,28 +14364,28 @@ function DeliveryTargetSettingsPage({
|
||||||
onBack
|
onBack
|
||||||
}) {
|
}) {
|
||||||
const definition = CHANNEL_DEFINITIONS[channel4];
|
const definition = CHANNEL_DEFINITIONS[channel4];
|
||||||
const [activeTabId, setActiveTabId] = React23.useState(BOT_SETTINGS_TABS[0].id);
|
const [activeTabId, setActiveTabId] = React24.useState(BOT_SETTINGS_TABS[0].id);
|
||||||
const [phase, setPhase] = React23.useState("loading");
|
const [phase, setPhase] = React24.useState("loading");
|
||||||
const [targets, setTargets] = React23.useState([]);
|
const [targets, setTargets] = React24.useState([]);
|
||||||
const [suggestionPhase, setSuggestionPhase] = React23.useState("idle");
|
const [suggestionPhase, setSuggestionPhase] = React24.useState("idle");
|
||||||
const [suggestions, setSuggestions] = React23.useState([]);
|
const [suggestions, setSuggestions] = React24.useState([]);
|
||||||
const [suggestionError, setSuggestionError] = React23.useState(null);
|
const [suggestionError, setSuggestionError] = React24.useState(null);
|
||||||
const [error, setError] = React23.useState(null);
|
const [error, setError] = React24.useState(null);
|
||||||
const [editor, setEditor] = React23.useState(null);
|
const [editor, setEditor] = React24.useState(null);
|
||||||
const [saving, setSaving] = React23.useState(false);
|
const [saving, setSaving] = React24.useState(false);
|
||||||
const [botCopyState, setBotCopyState] = React23.useState(null);
|
const [botCopyState, setBotCopyState] = React24.useState(null);
|
||||||
const [accessPolicy, setAccessPolicy] = React23.useState(account.accessPolicy);
|
const [accessPolicy, setAccessPolicy] = React24.useState(account.accessPolicy);
|
||||||
const [accessGrant, setAccessGrant] = React23.useState(account.accessGrant);
|
const [accessGrant, setAccessGrant] = React24.useState(account.accessGrant);
|
||||||
const mounted = React23.useRef(true);
|
const mounted = React24.useRef(true);
|
||||||
React23.useEffect(() => {
|
React24.useEffect(() => {
|
||||||
setAccessPolicy(account.accessPolicy);
|
setAccessPolicy(account.accessPolicy);
|
||||||
setAccessGrant(account.accessGrant);
|
setAccessGrant(account.accessGrant);
|
||||||
}, [account.botId, account.accessPolicy, account.accessGrant]);
|
}, [account.botId, account.accessPolicy, account.accessGrant]);
|
||||||
const invoke = React23.useCallback(async (endpoint, payload = {}, signal) => {
|
const invoke = React24.useCallback(async (endpoint, payload = {}, signal) => {
|
||||||
if (typeof rpcCall !== "function") throw new Error("\u6295\u9012\u76EE\u6807\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
|
if (typeof rpcCall !== "function") throw new Error("\u6295\u9012\u76EE\u6807\u8BBE\u7F6E\u6682\u4E0D\u53EF\u7528\u3002");
|
||||||
return unwrapRpcResult12(await rpcCall(endpoint, payload, signal));
|
return unwrapRpcResult12(await rpcCall(endpoint, payload, signal));
|
||||||
}, [rpcCall]);
|
}, [rpcCall]);
|
||||||
const loadTargets = React23.useCallback(async ({ signal, silent = false } = {}) => {
|
const loadTargets = React24.useCallback(async ({ signal, silent = false } = {}) => {
|
||||||
if (!silent) setPhase("loading");
|
if (!silent) setPhase("loading");
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
|
@ -14208,7 +14399,7 @@ function DeliveryTargetSettingsPage({
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
}
|
}
|
||||||
}, [account.botId, invoke]);
|
}, [account.botId, invoke]);
|
||||||
const loadSuggestions = React23.useCallback(async () => {
|
const loadSuggestions = React24.useCallback(async () => {
|
||||||
setSuggestionPhase("loading");
|
setSuggestionPhase("loading");
|
||||||
setSuggestionError(null);
|
setSuggestionError(null);
|
||||||
try {
|
try {
|
||||||
|
|
@ -14222,7 +14413,7 @@ function DeliveryTargetSettingsPage({
|
||||||
setSuggestionPhase("error");
|
setSuggestionPhase("error");
|
||||||
}
|
}
|
||||||
}, [account.botId, definition, invoke]);
|
}, [account.botId, definition, invoke]);
|
||||||
React23.useEffect(() => {
|
React24.useEffect(() => {
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
void loadTargets({ signal: controller.signal });
|
void loadTargets({ signal: controller.signal });
|
||||||
|
|
@ -14349,7 +14540,7 @@ function DeliveryTargetSettingsPage({
|
||||||
rpcCall: accessRpcCall,
|
rpcCall: accessRpcCall,
|
||||||
onSaved: setAccessPolicy
|
onSaved: setAccessPolicy
|
||||||
}) : h2(
|
}) : h2(
|
||||||
React23.Fragment,
|
React24.Fragment,
|
||||||
null,
|
null,
|
||||||
h2(
|
h2(
|
||||||
"section",
|
"section",
|
||||||
|
|
@ -15170,7 +15361,7 @@ function installImStyles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// plugin-src/client/update-panel.js
|
// plugin-src/client/update-panel.js
|
||||||
var React24 = __toESM(require("react"), 1);
|
var React25 = __toESM(require("react"), 1);
|
||||||
var import_react_dom3 = require("react-dom");
|
var import_react_dom3 = require("react-dom");
|
||||||
var import_valid = __toESM(require_valid(), 1);
|
var import_valid = __toESM(require_valid(), 1);
|
||||||
var import_rcompare = __toESM(require_rcompare(), 1);
|
var import_rcompare = __toESM(require_rcompare(), 1);
|
||||||
|
|
@ -15260,11 +15451,11 @@ function manualUpdateCommand(snapshot) {
|
||||||
return `dsh plugin --profile ${profileArgument} add -w dsh-im-ops@${version}`;
|
return `dsh plugin --profile ${profileArgument} add -w dsh-im-ops@${version}`;
|
||||||
}
|
}
|
||||||
function ManualUpdateCommand({ command, disabled, sourceInstall, desktop }) {
|
function ManualUpdateCommand({ command, disabled, sourceInstall, desktop }) {
|
||||||
const [copyState, setCopyState] = React24.useState("idle");
|
const [copyState, setCopyState] = React25.useState("idle");
|
||||||
const commandRef = React24.useRef(null);
|
const commandRef = React25.useRef(null);
|
||||||
const mounted = React24.useRef(false);
|
const mounted = React25.useRef(false);
|
||||||
const copying = React24.useRef(false);
|
const copying = React25.useRef(false);
|
||||||
React24.useEffect(() => {
|
React25.useEffect(() => {
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
return () => {
|
return () => {
|
||||||
mounted.current = false;
|
mounted.current = false;
|
||||||
|
|
@ -15296,7 +15487,7 @@ function ManualUpdateCommand({ command, disabled, sourceInstall, desktop }) {
|
||||||
{ className: "dim-updateManual", "aria-label": "\u624B\u5DE5\u66F4\u65B0" },
|
{ className: "dim-updateManual", "aria-label": "\u624B\u5DE5\u66F4\u65B0" },
|
||||||
h2("h4", { className: "dim-updateManualHeading" }, "\u624B\u5DE5\u66F4\u65B0"),
|
h2("h4", { className: "dim-updateManualHeading" }, "\u624B\u5DE5\u66F4\u65B0"),
|
||||||
command ? h2(
|
command ? h2(
|
||||||
React24.Fragment,
|
React25.Fragment,
|
||||||
null,
|
null,
|
||||||
h2("p", { className: "dim-updateManualHint" }, "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\u53EF\u4EE5\u4F7F\u7528\u547D\u4EE4\u66F4\u65B0\uFF1A"),
|
h2("p", { className: "dim-updateManualHint" }, "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\u53EF\u4EE5\u4F7F\u7528\u547D\u4EE4\u66F4\u65B0\uFF1A"),
|
||||||
h2(
|
h2(
|
||||||
|
|
@ -15332,7 +15523,7 @@ function ManualUpdateCommand({ command, disabled, sourceInstall, desktop }) {
|
||||||
"aria-hidden": "true",
|
"aria-hidden": "true",
|
||||||
focusable: "false"
|
focusable: "false"
|
||||||
}, copyState === "copied" ? h2("path", { d: "m4 10 4 4 8-8" }) : h2(
|
}, copyState === "copied" ? h2("path", { d: "m4 10 4 4 8-8" }) : h2(
|
||||||
React24.Fragment,
|
React25.Fragment,
|
||||||
null,
|
null,
|
||||||
h2("rect", { x: 7, y: 7, width: 10, height: 11, rx: 1.5 }),
|
h2("rect", { x: 7, y: 7, width: 10, height: 11, rx: 1.5 }),
|
||||||
h2("path", { d: "M5 13H3.5A1.5 1.5 0 0 1 2 11.5v-8A1.5 1.5 0 0 1 3.5 2h8A1.5 1.5 0 0 1 13 3.5V5" })
|
h2("path", { d: "M5 13H3.5A1.5 1.5 0 0 1 2 11.5v-8A1.5 1.5 0 0 1 3.5 2h8A1.5 1.5 0 0 1 13 3.5V5" })
|
||||||
|
|
@ -15357,10 +15548,10 @@ function ManualUpdateCommand({ command, disabled, sourceInstall, desktop }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function UpdateDialog({ children, onClose }) {
|
function UpdateDialog({ children, onClose }) {
|
||||||
const dialogRef = React24.useRef(null);
|
const dialogRef = React25.useRef(null);
|
||||||
const titleId = React24.useId();
|
const titleId = React25.useId();
|
||||||
const descriptionId = React24.useId();
|
const descriptionId = React25.useId();
|
||||||
React24.useEffect(() => {
|
React25.useEffect(() => {
|
||||||
const previous = globalThis.document?.activeElement;
|
const previous = globalThis.document?.activeElement;
|
||||||
dialogRef.current?.focus?.();
|
dialogRef.current?.focus?.();
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -15418,19 +15609,19 @@ function UpdateDialog({ children, onClose }) {
|
||||||
return typeof document !== "undefined" && document.body ? (0, import_react_dom3.createPortal)(content, document.body) : content;
|
return typeof document !== "undefined" && document.body ? (0, import_react_dom3.createPortal)(content, document.body) : content;
|
||||||
}
|
}
|
||||||
function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
const [snapshot, setSnapshot] = React24.useState(null);
|
const [snapshot, setSnapshot] = React25.useState(null);
|
||||||
const [action, setAction] = React24.useState("status");
|
const [action, setAction] = React25.useState("status");
|
||||||
const [error, setError] = React24.useState(null);
|
const [error, setError] = React25.useState(null);
|
||||||
const [open, setOpen] = React24.useState(false);
|
const [open, setOpen] = React25.useState(false);
|
||||||
const [uncertainInstall, setUncertainInstall] = React24.useState(false);
|
const [uncertainInstall, setUncertainInstall] = React25.useState(false);
|
||||||
const mounted = React24.useRef(false);
|
const mounted = React25.useRef(false);
|
||||||
const busy = React24.useRef(false);
|
const busy = React25.useRef(false);
|
||||||
const readController = React24.useRef(null);
|
const readController = React25.useRef(null);
|
||||||
const pollReadController = React24.useRef(null);
|
const pollReadController = React25.useRef(null);
|
||||||
const installRequest = React24.useRef(null);
|
const installRequest = React25.useRef(null);
|
||||||
const onStatusRef = React24.useRef(onStatus);
|
const onStatusRef = React25.useRef(onStatus);
|
||||||
onStatusRef.current = onStatus;
|
onStatusRef.current = onStatus;
|
||||||
const accept = React24.useCallback((next) => {
|
const accept = React25.useCallback((next) => {
|
||||||
setSnapshot(next);
|
setSnapshot(next);
|
||||||
onStatusRef.current?.(next);
|
onStatusRef.current?.(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -15439,7 +15630,7 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
setUncertainInstall(false);
|
setUncertainInstall(false);
|
||||||
accept(next);
|
accept(next);
|
||||||
};
|
};
|
||||||
const invoke = React24.useCallback(async (endpoint, payload = {}, signal) => {
|
const invoke = React25.useCallback(async (endpoint, payload = {}, signal) => {
|
||||||
if (typeof rpcCall !== "function") {
|
if (typeof rpcCall !== "function") {
|
||||||
const unavailable = new Error("\u5F53\u524D Host \u4E0D\u652F\u6301\u66F4\u65B0\u63A5\u53E3\uFF0C\u8BF7\u5148\u624B\u52A8\u66F4\u65B0\u63D2\u4EF6\u5E76\u91CD\u542F\u3002");
|
const unavailable = new Error("\u5F53\u524D Host \u4E0D\u652F\u6301\u66F4\u65B0\u63A5\u53E3\uFF0C\u8BF7\u5148\u624B\u52A8\u66F4\u65B0\u63D2\u4EF6\u5E76\u91CD\u542F\u3002");
|
||||||
unavailable.code = "update-unavailable";
|
unavailable.code = "update-unavailable";
|
||||||
|
|
@ -15447,7 +15638,7 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
}
|
}
|
||||||
return unwrapSnapshot(await rpcCall(endpoint, payload, signal));
|
return unwrapSnapshot(await rpcCall(endpoint, payload, signal));
|
||||||
}, [rpcCall]);
|
}, [rpcCall]);
|
||||||
React24.useEffect(() => {
|
React25.useEffect(() => {
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
busy.current = true;
|
busy.current = true;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -15470,7 +15661,7 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
const activeJob = ACTIVE_STATES4.has(snapshot?.job?.state);
|
const activeJob = ACTIVE_STATES4.has(snapshot?.job?.state);
|
||||||
const restartRequired = snapshot?.job?.state === "restart-required" || snapshot?.blockedReason === "pending-restart";
|
const restartRequired = snapshot?.job?.state === "restart-required" || snapshot?.blockedReason === "pending-restart";
|
||||||
const shouldPoll = activeJob || uncertainInstall;
|
const shouldPoll = activeJob || uncertainInstall;
|
||||||
React24.useEffect(() => {
|
React25.useEffect(() => {
|
||||||
if (!shouldPoll) return void 0;
|
if (!shouldPoll) return void 0;
|
||||||
let controller;
|
let controller;
|
||||||
const scheduler = createPollScheduler({
|
const scheduler = createPollScheduler({
|
||||||
|
|
@ -15568,7 +15759,7 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
const manualCommand = manualUpdateCommand(snapshot);
|
const manualCommand = manualUpdateCommand(snapshot);
|
||||||
const buttonLabel = action === "checking" ? "\u68C0\u67E5\u4E2D\u2026" : action === "starting" || activeJob ? "\u6B63\u5728\u66F4\u65B0\u2026" : restartRequired ? "\u5F85\u624B\u52A8\u91CD\u542F" : snapshot?.canInstall ? "\u66F4\u65B0\u81F3" : "\u68C0\u67E5\u66F4\u65B0";
|
const buttonLabel = action === "checking" ? "\u68C0\u67E5\u4E2D\u2026" : action === "starting" || activeJob ? "\u6B63\u5728\u66F4\u65B0\u2026" : restartRequired ? "\u5F85\u624B\u52A8\u91CD\u542F" : snapshot?.canInstall ? "\u66F4\u65B0\u81F3" : "\u68C0\u67E5\u66F4\u65B0";
|
||||||
return h2(
|
return h2(
|
||||||
React24.Fragment,
|
React25.Fragment,
|
||||||
null,
|
null,
|
||||||
h2("button", {
|
h2("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
|
|
@ -15593,13 +15784,13 @@ function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
||||||
h2("dt", null, "\u8FD0\u884C\u7248\u672C"),
|
h2("dt", null, "\u8FD0\u884C\u7248\u672C"),
|
||||||
h2("dd", null, `v${snapshot?.runningVersion ?? clientVersion}`),
|
h2("dd", null, `v${snapshot?.runningVersion ?? clientVersion}`),
|
||||||
snapshot?.installedVersion && snapshot.installedVersion !== snapshot.runningVersion ? h2(
|
snapshot?.installedVersion && snapshot.installedVersion !== snapshot.runningVersion ? h2(
|
||||||
React24.Fragment,
|
React25.Fragment,
|
||||||
null,
|
null,
|
||||||
h2("dt", null, "\u5DF2\u5B89\u88C5\u7248\u672C"),
|
h2("dt", null, "\u5DF2\u5B89\u88C5\u7248\u672C"),
|
||||||
h2("dd", null, `v${snapshot.installedVersion}`)
|
h2("dd", null, `v${snapshot.installedVersion}`)
|
||||||
) : null,
|
) : null,
|
||||||
targetVersion ? h2(
|
targetVersion ? h2(
|
||||||
React24.Fragment,
|
React25.Fragment,
|
||||||
null,
|
null,
|
||||||
h2("dt", null, "\u76EE\u6807\u7248\u672C"),
|
h2("dt", null, "\u76EE\u6807\u7248\u672C"),
|
||||||
h2("dd", null, `v${targetVersion}`)
|
h2("dd", null, `v${targetVersion}`)
|
||||||
|
|
@ -15812,20 +16003,20 @@ function IMSettingsTab({
|
||||||
browserLocation = globalThis.location,
|
browserLocation = globalThis.location,
|
||||||
navigateToRecoveryUrl = replacePageLocation
|
navigateToRecoveryUrl = replacePageLocation
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = React25.useState("weixin");
|
const [selected, setSelected] = React26.useState("weixin");
|
||||||
const [loopbackRecovery, setLoopbackRecovery] = React25.useState(null);
|
const [loopbackRecovery, setLoopbackRecovery] = React26.useState(null);
|
||||||
const [runningVersion, setRunningVersion] = React25.useState(IM_PLUGIN_VERSION);
|
const [runningVersion, setRunningVersion] = React26.useState(IM_PLUGIN_VERSION);
|
||||||
const [deliverySettings, setDeliverySettings] = React25.useState(null);
|
const [deliverySettings, setDeliverySettings] = React26.useState(null);
|
||||||
const githubTooltipId = React25.useId();
|
const githubTooltipId = React26.useId();
|
||||||
const channelSelectId = React25.useId();
|
const channelSelectId = React26.useId();
|
||||||
const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
|
const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
|
||||||
const reportLoopbackRecovery = React25.useCallback((recovery) => {
|
const reportLoopbackRecovery = React26.useCallback((recovery) => {
|
||||||
setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
|
setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
|
||||||
}, []);
|
}, []);
|
||||||
const reportUpdateStatus = React25.useCallback((snapshot) => {
|
const reportUpdateStatus = React26.useCallback((snapshot) => {
|
||||||
setRunningVersion(snapshot.runningVersion);
|
setRunningVersion(snapshot.runningVersion);
|
||||||
}, []);
|
}, []);
|
||||||
const rpcCalls = React25.useMemo(() => createLoopbackAwareRpcCalls({
|
const rpcCalls = React26.useMemo(() => createLoopbackAwareRpcCalls({
|
||||||
dingtalkRpcCall,
|
dingtalkRpcCall,
|
||||||
discordRpcCall,
|
discordRpcCall,
|
||||||
feishuRpcCall,
|
feishuRpcCall,
|
||||||
|
|
@ -15857,7 +16048,7 @@ function IMSettingsTab({
|
||||||
weixinRpcCall,
|
weixinRpcCall,
|
||||||
whatsappRpcCall
|
whatsappRpcCall
|
||||||
]);
|
]);
|
||||||
const botSettingsContext = React25.useMemo(() => Object.freeze({
|
const botSettingsContext = React26.useMemo(() => Object.freeze({
|
||||||
openBotSettings: setDeliverySettings
|
openBotSettings: setDeliverySettings
|
||||||
}), []);
|
}), []);
|
||||||
return h2(
|
return h2(
|
||||||
|
|
|
||||||
460
lib/index.js
460
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,9 @@
|
||||||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||||
|
import {
|
||||||
|
normalizeDefaultModelSelection,
|
||||||
|
normalizeHostModelCatalog,
|
||||||
|
SET_DEFAULT_MODEL_ENDPOINT,
|
||||||
|
} from '../../default-model.js';
|
||||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||||
import { normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
|
import { normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
|
||||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||||
|
|
@ -21,6 +26,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
setGroupSessionScope: 'bot.group-session-scope.set',
|
setGroupSessionScope: 'bot.group-session-scope.set',
|
||||||
setWorkspace: 'bot.workspace.set',
|
setWorkspace: 'bot.workspace.set',
|
||||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||||
|
setDefaultModel: SET_DEFAULT_MODEL_ENDPOINT,
|
||||||
setContextEnhancement: 'bot.context-enhancement.set',
|
setContextEnhancement: 'bot.context-enhancement.set',
|
||||||
resolveChannelPeer: 'bot.session.channel-peer',
|
resolveChannelPeer: 'bot.session.channel-peer',
|
||||||
});
|
});
|
||||||
|
|
@ -98,6 +104,7 @@ function normalizeBot(value) {
|
||||||
state: connected ? 'connected' : state,
|
state: connected ? 'connected' : state,
|
||||||
workspace: text(value.workspace, '', 4_096),
|
workspace: text(value.workspace, '', 4_096),
|
||||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||||
|
defaultModel: normalizeDefaultModelSelection(value.defaultModel),
|
||||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||||
...(Object.hasOwn(value, 'accessPolicy')
|
...(Object.hasOwn(value, 'accessPolicy')
|
||||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||||
|
|
@ -137,6 +144,7 @@ export function normalizeSnapshot(value) {
|
||||||
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
||||||
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
||||||
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
|
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
|
||||||
|
modelCatalog: normalizeHostModelCatalog(source.modelCatalog),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import {
|
||||||
AgentPresetEditor,
|
AgentPresetEditor,
|
||||||
EMPTY_AGENT_PRESET_CATALOG,
|
EMPTY_AGENT_PRESET_CATALOG,
|
||||||
} from '../../agent-preset.js';
|
} from '../../agent-preset.js';
|
||||||
|
import {
|
||||||
|
DefaultModelEditor,
|
||||||
|
EMPTY_MODEL_CATALOG,
|
||||||
|
ModelCatalogContext,
|
||||||
|
} from '../../default-model.js';
|
||||||
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
||||||
import {
|
import {
|
||||||
BotSettingsButton,
|
BotSettingsButton,
|
||||||
|
|
@ -186,6 +191,7 @@ export function WhatsappAccountCard({
|
||||||
onReconnect,
|
onReconnect,
|
||||||
onWorkspaceSave,
|
onWorkspaceSave,
|
||||||
onAgentPresetSave,
|
onAgentPresetSave,
|
||||||
|
onDefaultModelSave,
|
||||||
onContextEnhancementSave,
|
onContextEnhancementSave,
|
||||||
onRequestRemove,
|
onRequestRemove,
|
||||||
onConfirmRemove,
|
onConfirmRemove,
|
||||||
|
|
@ -233,6 +239,11 @@ export function WhatsappAccountCard({
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
onSave: onAgentPresetSave,
|
onSave: onAgentPresetSave,
|
||||||
}),
|
}),
|
||||||
|
h(DefaultModelEditor, {
|
||||||
|
defaultModel: account.defaultModel,
|
||||||
|
disabled: Boolean(busy),
|
||||||
|
onSave: onDefaultModelSave,
|
||||||
|
}),
|
||||||
h(ContextEnhancementEditor, {
|
h(ContextEnhancementEditor, {
|
||||||
config: account.contextEnhancement,
|
config: account.contextEnhancement,
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
|
|
@ -268,6 +279,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
const [model, setModel] = React.useState({
|
const [model, setModel] = React.useState({
|
||||||
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
||||||
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: EMPTY_MODEL_CATALOG,
|
||||||
});
|
});
|
||||||
const [provision, setProvision] = React.useState(null);
|
const [provision, setProvision] = React.useState(null);
|
||||||
const [busy, setBusy] = React.useState(false);
|
const [busy, setBusy] = React.useState(false);
|
||||||
|
|
@ -306,6 +318,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
setModel({
|
setModel({
|
||||||
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
||||||
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
||||||
});
|
});
|
||||||
if (restore && snapshot.provisioning) setProvision({
|
if (restore && snapshot.provisioning) setProvision({
|
||||||
...snapshot.provisioning,
|
...snapshot.provisioning,
|
||||||
|
|
@ -437,6 +450,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
setModel({
|
setModel({
|
||||||
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
||||||
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
||||||
});
|
});
|
||||||
if (operation === 'reconnect') {
|
if (operation === 'reconnect') {
|
||||||
setTestNoticeByBot((current) => ({
|
setTestNoticeByBot((current) => ({
|
||||||
|
|
@ -495,6 +509,12 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
WHATSAPP_ENDPOINTS.setAgentPreset,
|
WHATSAPP_ENDPOINTS.setAgentPreset,
|
||||||
{ botId: account.botId, agentPreset },
|
{ botId: account.botId, agentPreset },
|
||||||
),
|
),
|
||||||
|
onDefaultModelSave: (defaultModel) => botAction(
|
||||||
|
account,
|
||||||
|
'model',
|
||||||
|
WHATSAPP_ENDPOINTS.setDefaultModel,
|
||||||
|
{ botId: account.botId, defaultModel },
|
||||||
|
),
|
||||||
onContextEnhancementSave: (config) => botAction(
|
onContextEnhancementSave: (config) => botAction(
|
||||||
account,
|
account,
|
||||||
'context-enhancement',
|
'context-enhancement',
|
||||||
|
|
@ -515,6 +535,8 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
|
|
||||||
return h(AgentPresetCatalogContext.Provider, {
|
return h(AgentPresetCatalogContext.Provider, {
|
||||||
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
||||||
|
}, h(ModelCatalogContext.Provider, {
|
||||||
|
value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
||||||
}, h('section', {
|
}, h('section', {
|
||||||
className: 'ddt-page dwa-page dim-channelPage',
|
className: 'ddt-page dwa-page dim-channelPage',
|
||||||
'aria-label': 'WhatsApp 设置',
|
'aria-label': 'WhatsApp 设置',
|
||||||
|
|
@ -552,5 +574,5 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
||||||
: model.bots.length === 0
|
: model.bots.length === 0
|
||||||
? h(EmptyView, { busy, onStart: () => void startProvisioning(false) })
|
? h(EmptyView, { busy, onStart: () => void startProvisioning(false) })
|
||||||
: null,
|
: null,
|
||||||
botList)));
|
botList))));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
105
plugin-src/client/default-model.js
Normal file
105
plugin-src/client/default-model.js
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultModelKey,
|
||||||
|
normalizeDefaultModel,
|
||||||
|
normalizeModelCatalog,
|
||||||
|
} from '../../src/channels/shared/default-model.mjs';
|
||||||
|
import { h } from './i18n.js';
|
||||||
|
|
||||||
|
export const SET_DEFAULT_MODEL_ENDPOINT = 'bot.model.set';
|
||||||
|
|
||||||
|
export const EMPTY_MODEL_CATALOG = Object.freeze({
|
||||||
|
groups: Object.freeze([]),
|
||||||
|
failures: Object.freeze([]),
|
||||||
|
items: Object.freeze([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ModelCatalogContext = React.createContext(EMPTY_MODEL_CATALOG);
|
||||||
|
|
||||||
|
export function normalizeDefaultModelSelection(value) {
|
||||||
|
return normalizeDefaultModel(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeHostModelCatalog(value) {
|
||||||
|
return normalizeModelCatalog(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DefaultModelEditor({ defaultModel = null, disabled = false, onSave }) {
|
||||||
|
const catalog = React.useContext(ModelCatalogContext) ?? EMPTY_MODEL_CATALOG;
|
||||||
|
const helpId = React.useId();
|
||||||
|
const current = normalizeDefaultModel(defaultModel);
|
||||||
|
const currentKey = defaultModelKey(current);
|
||||||
|
const [saving, setSaving] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState(null);
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const item of Array.isArray(catalog.items) ? catalog.items : []) {
|
||||||
|
if (!item?.key || seen.has(item.key)) continue;
|
||||||
|
seen.add(item.key);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
const currentUnavailable = Boolean(currentKey && !seen.has(currentKey));
|
||||||
|
if (currentUnavailable && current) {
|
||||||
|
items.push({
|
||||||
|
key: currentKey,
|
||||||
|
provider: current.provider,
|
||||||
|
model: current.model,
|
||||||
|
label: currentKey,
|
||||||
|
unavailable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = async (event) => {
|
||||||
|
const nextKey = event.target.value;
|
||||||
|
if (nextKey === currentKey || saving || disabled) return;
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await onSave?.(nextKey ? normalizeDefaultModel(nextKey) : null);
|
||||||
|
} catch (cause) {
|
||||||
|
setError(cause?.message ?? '默认模型修改失败,请重试。');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return h('div', { className: 'dim-preset' },
|
||||||
|
h('div', { className: 'dim-presetHeader' },
|
||||||
|
h('span', { className: 'dim-presetTitle' },
|
||||||
|
h('span', null, '默认模型'),
|
||||||
|
h('span', { className: 'dim-presetHelp' },
|
||||||
|
h('button', {
|
||||||
|
type: 'button',
|
||||||
|
className: 'dim-presetHelpButton',
|
||||||
|
'aria-label': '查看默认模型说明',
|
||||||
|
'aria-describedby': helpId,
|
||||||
|
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
||||||
|
h('span', {
|
||||||
|
id: helpId,
|
||||||
|
className: 'dim-presetTooltip',
|
||||||
|
role: 'tooltip',
|
||||||
|
}, '为该 WhatsApp 机器人的新建会话指定模型,避免每人各自 /model。已有会话需先 /new 再发消息才会生效。'))),
|
||||||
|
saving ? h('span', { className: 'dim-presetStatus' }, '保存中…') : null),
|
||||||
|
React.createElement('select', {
|
||||||
|
className: 'dim-presetSelect',
|
||||||
|
value: currentKey,
|
||||||
|
disabled: disabled || saving,
|
||||||
|
'aria-label': '默认模型',
|
||||||
|
onChange: (event) => { void change(event); },
|
||||||
|
},
|
||||||
|
h('option', { value: '' }, '跟随 Host 默认'),
|
||||||
|
...items.map((item) => h(
|
||||||
|
'option',
|
||||||
|
{ key: item.key, value: item.key },
|
||||||
|
item.unavailable ? [item.key, '(已不可用)'] : item.label,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
error || currentUnavailable ? h(
|
||||||
|
'p',
|
||||||
|
{ className: 'dim-presetError', role: error ? 'alert' : 'status' },
|
||||||
|
error ?? '当前默认模型已不可用,请选择其他模型或跟随 Host 默认。',
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
27
plugin-src/host/channels/shared/default-model-rpc.mjs
Normal file
27
plugin-src/host/channels/shared/default-model-rpc.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { normalizeDefaultModel } from '../../../../src/channels/shared/default-model.mjs';
|
||||||
|
|
||||||
|
export const SET_DEFAULT_MODEL_ENDPOINT = 'bot.model.set';
|
||||||
|
|
||||||
|
function isRecord(value) {
|
||||||
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validDefaultModelPayload(payload) {
|
||||||
|
if (!isRecord(payload)
|
||||||
|
|| !Object.keys(payload).every((key) => ['botId', 'defaultModel'].includes(key))
|
||||||
|
|| typeof payload.botId !== 'string'
|
||||||
|
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (payload.defaultModel === null) return true;
|
||||||
|
return Boolean(normalizeDefaultModel(payload.defaultModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publicDefaultModelError(error) {
|
||||||
|
if (![
|
||||||
|
'default-model-invalid',
|
||||||
|
'default-model-unavailable',
|
||||||
|
'workspace-bot-not-found',
|
||||||
|
].includes(error?.code)) return null;
|
||||||
|
return { code: error.code, message: error.message };
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,8 @@ export function publicWorkspaceError(error) {
|
||||||
'workspace-bot-not-found',
|
'workspace-bot-not-found',
|
||||||
'agent-preset-invalid',
|
'agent-preset-invalid',
|
||||||
'agent-preset-unavailable',
|
'agent-preset-unavailable',
|
||||||
|
'default-model-invalid',
|
||||||
|
'default-model-unavailable',
|
||||||
'context-enhancement-invalid',
|
'context-enhancement-invalid',
|
||||||
].includes(error?.code)) return null;
|
].includes(error?.code)) return null;
|
||||||
return { code: error.code, message: error.message };
|
return { code: error.code, message: error.message };
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
||||||
workspaces,
|
workspaces,
|
||||||
stateFor,
|
stateFor,
|
||||||
agentPresetCatalog,
|
agentPresetCatalog,
|
||||||
|
modelCatalog: () => harness.listModels(),
|
||||||
channel: 'whatsapp',
|
channel: 'whatsapp',
|
||||||
});
|
});
|
||||||
const supervisor = createSupervisor({
|
const supervisor = createSupervisor({
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.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';
|
||||||
|
import { SET_DEFAULT_MODEL_ENDPOINT, validDefaultModelPayload } from '../shared/default-model-rpc.mjs';
|
||||||
|
|
||||||
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
||||||
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
|
|
@ -32,6 +33,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
||||||
setGroupSessionScope: SET_GROUP_SESSION_SCOPE_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,
|
||||||
|
setDefaultModel: SET_DEFAULT_MODEL_ENDPOINT,
|
||||||
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
||||||
resolveChannelPeer: 'bot.session.channel-peer',
|
resolveChannelPeer: 'bot.session.channel-peer',
|
||||||
});
|
});
|
||||||
|
|
@ -97,6 +99,10 @@ function payloadFailure(endpoint, payload) {
|
||||||
return validAgentPresetPayload(payload)
|
return validAgentPresetPayload(payload)
|
||||||
? null : '请选择 Agent Preset。';
|
? null : '请选择 Agent Preset。';
|
||||||
}
|
}
|
||||||
|
if (endpoint === WHATSAPP_ENDPOINTS.setDefaultModel) {
|
||||||
|
return validDefaultModelPayload(payload)
|
||||||
|
? null : '请选择默认模型。';
|
||||||
|
}
|
||||||
if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
|
if (endpoint === WHATSAPP_ENDPOINTS.setContextEnhancement) {
|
||||||
return validContextEnhancementPayload(payload)
|
return validContextEnhancementPayload(payload)
|
||||||
? null : '请提交有效的上下文增强设置。';
|
? null : '请提交有效的上下文增强设置。';
|
||||||
|
|
@ -221,6 +227,12 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
||||||
await controller.updateAgentPreset(payload.botId, payload.agentPreset),
|
await controller.updateAgentPreset(payload.botId, payload.agentPreset),
|
||||||
cachedEncode,
|
cachedEncode,
|
||||||
);
|
);
|
||||||
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setDefaultModel) {
|
||||||
|
if (typeof controller.updateDefaultModel !== 'function') throw new Error('Default model update is unavailable');
|
||||||
|
value = await publicStatus(
|
||||||
|
await controller.updateDefaultModel(payload.botId, payload.defaultModel),
|
||||||
|
cachedEncode,
|
||||||
|
);
|
||||||
} else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
|
} else if (endpoint === WHATSAPP_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(
|
value = await controller.updateAccessPolicy(
|
||||||
|
|
|
||||||
|
|
@ -473,6 +473,7 @@ class ModernHarnessApi {
|
||||||
const claimed = new Set([...this.#pendingApprovals.values()].map((entry) => entry.approvalId));
|
const claimed = new Set([...this.#pendingApprovals.values()].map((entry) => entry.approvalId));
|
||||||
const decided = new Set();
|
const decided = new Set();
|
||||||
let approvalId;
|
let approvalId;
|
||||||
|
let fallbackId;
|
||||||
for (let index = owner.events.length - 1; index >= 0; index -= 1) {
|
for (let index = owner.events.length - 1; index >= 0; index -= 1) {
|
||||||
const event = owner.events[index];
|
const event = owner.events[index];
|
||||||
if (event.type === 'approval/decided') {
|
if (event.type === 'approval/decided') {
|
||||||
|
|
@ -480,11 +481,20 @@ class ModernHarnessApi {
|
||||||
} else if (event.type === 'approval/asked') {
|
} else if (event.type === 'approval/asked') {
|
||||||
const id = event.data?.id;
|
const id = event.data?.id;
|
||||||
if (!id || decided.has(id) || claimed.has(id)) continue;
|
if (!id || decided.has(id) || claimed.has(id)) continue;
|
||||||
if ((request.callId ?? null) !== (event.data?.callId ?? null)) continue;
|
// Prefer an exact callId match; keep the newest unmatched ask as a
|
||||||
approvalId = id;
|
// fallback so a callId shape drift cannot silently hand the prompt to
|
||||||
break;
|
// DSH Web while the originating IM turn is still waiting.
|
||||||
|
if ((request.callId ?? null) === (event.data?.callId ?? null)) {
|
||||||
|
approvalId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (fallbackId === undefined
|
||||||
|
&& (request.toolName === undefined || request.toolName === event.data?.toolName)) {
|
||||||
|
fallbackId = id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
approvalId ??= fallbackId;
|
||||||
if (approvalId === undefined) return next();
|
if (approvalId === undefined) return next();
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const pending = {
|
const pending = {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ import {
|
||||||
normalizeAgentPresetCatalog,
|
normalizeAgentPresetCatalog,
|
||||||
validateAgentPresetId,
|
validateAgentPresetId,
|
||||||
} from './agent-preset.mjs';
|
} from './agent-preset.mjs';
|
||||||
|
import {
|
||||||
|
catalogHasModel,
|
||||||
|
normalizeModelCatalog,
|
||||||
|
validateDefaultModel,
|
||||||
|
} from './default-model.mjs';
|
||||||
import {
|
import {
|
||||||
normalizeAccessPolicy,
|
normalizeAccessPolicy,
|
||||||
validateAccessPolicy,
|
validateAccessPolicy,
|
||||||
|
|
@ -191,6 +196,21 @@ function normalizeDocument(value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let defaultModels = {};
|
||||||
|
if (value.defaultModels !== undefined) {
|
||||||
|
if (!value.defaultModels || typeof value.defaultModels !== 'object'
|
||||||
|
|| Array.isArray(value.defaultModels)) return null;
|
||||||
|
for (const [botId, defaultModel] of Object.entries(value.defaultModels)) {
|
||||||
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return null;
|
||||||
|
try {
|
||||||
|
const normalized = validateDefaultModel(defaultModel);
|
||||||
|
if (!normalized) return null;
|
||||||
|
defaultModels[botId] = normalized;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let groupSessionScopes = {};
|
let groupSessionScopes = {};
|
||||||
if (value.groupSessionScopes !== undefined) {
|
if (value.groupSessionScopes !== undefined) {
|
||||||
if (!value.groupSessionScopes || typeof value.groupSessionScopes !== 'object'
|
if (!value.groupSessionScopes || typeof value.groupSessionScopes !== 'object'
|
||||||
|
|
@ -235,6 +255,7 @@ function normalizeDocument(value) {
|
||||||
version,
|
version,
|
||||||
workspaces,
|
workspaces,
|
||||||
agentPresets,
|
agentPresets,
|
||||||
|
defaultModels,
|
||||||
groupSessionScopes,
|
groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
|
|
@ -247,6 +268,7 @@ function storedDocument({
|
||||||
version,
|
version,
|
||||||
workspaces,
|
workspaces,
|
||||||
agentPresets,
|
agentPresets,
|
||||||
|
defaultModels,
|
||||||
groupSessionScopes,
|
groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
|
|
@ -255,6 +277,7 @@ function storedDocument({
|
||||||
}) {
|
}) {
|
||||||
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(defaultModels).length > 0) document.defaultModels = defaultModels;
|
||||||
if (Object.keys(groupSessionScopes).length > 0) document.groupSessionScopes = groupSessionScopes;
|
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;
|
||||||
|
|
@ -310,6 +333,7 @@ export class BotWorkspaceStore {
|
||||||
#version = 1;
|
#version = 1;
|
||||||
#workspaces = {};
|
#workspaces = {};
|
||||||
#agentPresets = {};
|
#agentPresets = {};
|
||||||
|
#defaultModels = {};
|
||||||
#groupSessionScopes = {};
|
#groupSessionScopes = {};
|
||||||
#contextEnhancement = {};
|
#contextEnhancement = {};
|
||||||
#deliveryTargets = Object.create(null);
|
#deliveryTargets = Object.create(null);
|
||||||
|
|
@ -338,6 +362,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.#defaultModels = normalized.defaultModels;
|
||||||
this.#groupSessionScopes = normalized.groupSessionScopes;
|
this.#groupSessionScopes = normalized.groupSessionScopes;
|
||||||
this.#contextEnhancement = normalized.contextEnhancement;
|
this.#contextEnhancement = normalized.contextEnhancement;
|
||||||
this.#deliveryTargets = normalized.deliveryTargets;
|
this.#deliveryTargets = normalized.deliveryTargets;
|
||||||
|
|
@ -348,6 +373,7 @@ export class BotWorkspaceStore {
|
||||||
this.#version = 1;
|
this.#version = 1;
|
||||||
this.#workspaces = {};
|
this.#workspaces = {};
|
||||||
this.#agentPresets = {};
|
this.#agentPresets = {};
|
||||||
|
this.#defaultModels = {};
|
||||||
this.#groupSessionScopes = {};
|
this.#groupSessionScopes = {};
|
||||||
this.#contextEnhancement = {};
|
this.#contextEnhancement = {};
|
||||||
this.#deliveryTargets = Object.create(null);
|
this.#deliveryTargets = Object.create(null);
|
||||||
|
|
@ -384,6 +410,10 @@ export class BotWorkspaceStore {
|
||||||
return this.#agentPresets[botIdOf(botId)] ?? null;
|
return this.#agentPresets[botIdOf(botId)] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultModelFor(botId) {
|
||||||
|
return this.#defaultModels[botIdOf(botId)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
groupSessionScopeFor(botId) {
|
groupSessionScopeFor(botId) {
|
||||||
const id = botIdOf(botId);
|
const id = botIdOf(botId);
|
||||||
if (this.has(id) && Object.hasOwn(this.#groupSessionScopes, id)) {
|
if (this.has(id) && Object.hasOwn(this.#groupSessionScopes, id)) {
|
||||||
|
|
@ -654,6 +684,40 @@ export class BotWorkspaceStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDefaultModel(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 defaultModel = validateDefaultModel(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 = this.#defaultModels[id] ?? null;
|
||||||
|
const same = previous?.provider === defaultModel?.provider
|
||||||
|
&& previous?.model === defaultModel?.model
|
||||||
|
&& previous?.reasoningEffort === defaultModel?.reasoningEffort;
|
||||||
|
if (same) return defaultModel;
|
||||||
|
if (defaultModel) this.#defaultModels[id] = defaultModel;
|
||||||
|
else delete this.#defaultModels[id];
|
||||||
|
try {
|
||||||
|
await this.#persist();
|
||||||
|
} catch (error) {
|
||||||
|
if (previous) this.#defaultModels[id] = previous;
|
||||||
|
else delete this.#defaultModels[id];
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return defaultModel;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async setGroupSessionScope(botId, value, { incarnation } = {}) {
|
async setGroupSessionScope(botId, value, { incarnation } = {}) {
|
||||||
const id = botIdOf(botId);
|
const id = botIdOf(botId);
|
||||||
if (!this.has(id)
|
if (!this.has(id)
|
||||||
|
|
@ -949,6 +1013,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.#defaultModels),
|
||||||
...Object.keys(this.#groupSessionScopes),
|
...Object.keys(this.#groupSessionScopes),
|
||||||
...Object.keys(this.#contextEnhancement),
|
...Object.keys(this.#contextEnhancement),
|
||||||
...Object.keys(this.#deliveryTargets),
|
...Object.keys(this.#deliveryTargets),
|
||||||
|
|
@ -970,6 +1035,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),
|
||||||
|
defaultModel: this.defaultModelFor(bot.botId),
|
||||||
groupSessionScope: this.groupSessionScopeFor(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),
|
||||||
|
|
@ -1003,15 +1069,18 @@ 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 hadDefaultModel = Object.hasOwn(this.#defaultModels, id);
|
||||||
const hadGroupSessionScope = Object.hasOwn(this.#groupSessionScopes, 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 hadAccessGrant = Object.hasOwn(this.#accessGrants, id);
|
const hadAccessGrant = Object.hasOwn(this.#accessGrants, id);
|
||||||
const needsCleanup = hadWorkspace || hadPreset || hadGroupSessionScope || hadContextEnhancement
|
const needsCleanup = hadWorkspace || hadPreset || hadDefaultModel || hadGroupSessionScope
|
||||||
|
|| hadContextEnhancement
|
||||||
|| hadDeliveryTargets || hadAccessPolicy || hadAccessGrant || this.#dirtyRemovals.has(id);
|
|| hadDeliveryTargets || hadAccessPolicy || hadAccessGrant || this.#dirtyRemovals.has(id);
|
||||||
delete this.#workspaces[id];
|
delete this.#workspaces[id];
|
||||||
delete this.#agentPresets[id];
|
delete this.#agentPresets[id];
|
||||||
|
delete this.#defaultModels[id];
|
||||||
delete this.#groupSessionScopes[id];
|
delete this.#groupSessionScopes[id];
|
||||||
delete this.#contextEnhancement[id];
|
delete this.#contextEnhancement[id];
|
||||||
delete this.#deliveryTargets[id];
|
delete this.#deliveryTargets[id];
|
||||||
|
|
@ -1056,6 +1125,7 @@ export class BotWorkspaceStore {
|
||||||
version,
|
version,
|
||||||
workspaces: this.#workspaces,
|
workspaces: this.#workspaces,
|
||||||
agentPresets: this.#agentPresets,
|
agentPresets: this.#agentPresets,
|
||||||
|
defaultModels: this.#defaultModels,
|
||||||
groupSessionScopes: this.#groupSessionScopes,
|
groupSessionScopes: this.#groupSessionScopes,
|
||||||
contextEnhancement,
|
contextEnhancement,
|
||||||
deliveryTargets,
|
deliveryTargets,
|
||||||
|
|
@ -1068,6 +1138,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.#defaultModels).length > 0
|
||||||
|| Object.keys(this.#groupSessionScopes).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
|
||||||
|
|
@ -1107,17 +1178,41 @@ function assertCurrentBotScope(isCurrentScope) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decorateResult(workspaces, result, catalog) {
|
function decorateResult(workspaces, result, catalog, modelCatalog) {
|
||||||
const decorate = (value) => {
|
const decorate = (value) => {
|
||||||
const decorated = workspaces.decorateStatus(value);
|
const decorated = workspaces.decorateStatus(value);
|
||||||
if (!catalog || !decorated || typeof decorated !== 'object') return decorated;
|
if (!decorated || typeof decorated !== 'object') return decorated;
|
||||||
const attachCatalog = (agentPresetCatalog) => (
|
const attach = (agentPresetCatalog, resolvedModelCatalog) => {
|
||||||
agentPresetCatalog ? { ...decorated, agentPresetCatalog } : decorated
|
let next = decorated;
|
||||||
);
|
if (agentPresetCatalog) next = { ...next, agentPresetCatalog };
|
||||||
const agentPresetCatalog = resolveAgentPresetCatalog(catalog);
|
if (resolvedModelCatalog) next = { ...next, modelCatalog: resolvedModelCatalog };
|
||||||
return agentPresetCatalog && typeof agentPresetCatalog.then === 'function'
|
return next;
|
||||||
? agentPresetCatalog.then(attachCatalog)
|
};
|
||||||
: attachCatalog(agentPresetCatalog);
|
const agentPresetCatalog = catalog
|
||||||
|
? resolveAgentPresetCatalog(catalog)
|
||||||
|
: null;
|
||||||
|
const resolvedModels = modelCatalog
|
||||||
|
? Promise.resolve().then(async () => {
|
||||||
|
try {
|
||||||
|
const raw = typeof modelCatalog === 'function' ? await modelCatalog() : await modelCatalog;
|
||||||
|
return normalizeModelCatalog(raw);
|
||||||
|
} catch {
|
||||||
|
return normalizeModelCatalog(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (agentPresetCatalog && typeof agentPresetCatalog.then === 'function') {
|
||||||
|
if (resolvedModels) {
|
||||||
|
return Promise.all([agentPresetCatalog, resolvedModels]).then(([presets, models]) => (
|
||||||
|
attach(presets, models)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return agentPresetCatalog.then((presets) => attach(presets, null));
|
||||||
|
}
|
||||||
|
if (resolvedModels) {
|
||||||
|
return resolvedModels.then((models) => attach(agentPresetCatalog, models));
|
||||||
|
}
|
||||||
|
return attach(agentPresetCatalog, null);
|
||||||
};
|
};
|
||||||
return result && typeof result.then === 'function'
|
return result && typeof result.then === 'function'
|
||||||
? result.then(decorate)
|
? result.then(decorate)
|
||||||
|
|
@ -1358,6 +1453,19 @@ export function createBotWorkspaceScope(
|
||||||
...(agentPreset == null ? {} : { agentPreset }),
|
...(agentPreset == null ? {} : { agentPreset }),
|
||||||
});
|
});
|
||||||
sessionGenerations.set(sessionId, generation);
|
sessionGenerations.set(sessionId, generation);
|
||||||
|
const defaultModel = workspaces.defaultModelFor(botId);
|
||||||
|
if (defaultModel && typeof target.selectSessionModel === 'function') {
|
||||||
|
try {
|
||||||
|
await target.selectSessionModel(sessionId, defaultModel);
|
||||||
|
} catch (error) {
|
||||||
|
// Keep the new Session usable on Host default when the configured
|
||||||
|
// model disappeared; the settings page marks unavailable choices.
|
||||||
|
console.warn(
|
||||||
|
`[dsh-im] failed to apply default model for bot ${botId}:`,
|
||||||
|
error?.message ?? error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return sessionId;
|
return sessionId;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1522,6 +1630,7 @@ export function createWorkspaceAwareController(controller, {
|
||||||
workspaces,
|
workspaces,
|
||||||
stateFor,
|
stateFor,
|
||||||
agentPresetCatalog,
|
agentPresetCatalog,
|
||||||
|
modelCatalog = null,
|
||||||
channel = null,
|
channel = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!controller || !workspaces || typeof stateFor !== 'function') {
|
if (!controller || !workspaces || typeof stateFor !== 'function') {
|
||||||
|
|
@ -1537,7 +1646,7 @@ export function createWorkspaceAwareController(controller, {
|
||||||
if (transitions.get(botId) === current) transitions.delete(botId);
|
if (transitions.get(botId) === current) transitions.delete(botId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog);
|
const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog, modelCatalog);
|
||||||
const updateWorkspace = (botId, workspace) => {
|
const updateWorkspace = (botId, workspace) => {
|
||||||
// Capture at API invocation, before even waiting for an older outer
|
// Capture at API invocation, before even waiting for an older outer
|
||||||
// transition. A queued request still belongs to the incarnation that the
|
// transition. A queued request still belongs to the incarnation that the
|
||||||
|
|
@ -1580,6 +1689,40 @@ export function createWorkspaceAwareController(controller, {
|
||||||
workspaces,
|
workspaces,
|
||||||
await controller.status(),
|
await controller.status(),
|
||||||
catalog ?? agentPresetCatalog,
|
catalog ?? agentPresetCatalog,
|
||||||
|
modelCatalog,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const updateDefaultModel = (botId, defaultModel) => {
|
||||||
|
const incarnation = workspaces.incarnationFor(botId);
|
||||||
|
const normalized = validateDefaultModel(defaultModel);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
let catalog = null;
|
||||||
|
if (normalized && modelCatalog) {
|
||||||
|
try {
|
||||||
|
const raw = typeof modelCatalog === 'function' ? await modelCatalog() : await modelCatalog;
|
||||||
|
catalog = normalizeModelCatalog(raw);
|
||||||
|
} catch {
|
||||||
|
catalog = normalizeModelCatalog(null);
|
||||||
|
}
|
||||||
|
if (!catalogHasModel(catalog, normalized)) {
|
||||||
|
const error = new Error('默认模型不存在或当前不可用。');
|
||||||
|
error.code = 'default-model-unavailable';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await workspaces.setDefaultModel(botId, normalized, { incarnation });
|
||||||
|
return decorateResult(
|
||||||
|
workspaces,
|
||||||
|
await controller.status(),
|
||||||
|
agentPresetCatalog,
|
||||||
|
catalog ?? modelCatalog,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -1774,6 +1917,7 @@ export function createWorkspaceAwareController(controller, {
|
||||||
get(target, property) {
|
get(target, property) {
|
||||||
if (property === 'updateWorkspace') return updateWorkspace;
|
if (property === 'updateWorkspace') return updateWorkspace;
|
||||||
if (property === 'updateAgentPreset') return updateAgentPreset;
|
if (property === 'updateAgentPreset') return updateAgentPreset;
|
||||||
|
if (property === 'updateDefaultModel') return updateDefaultModel;
|
||||||
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;
|
if (property === 'updateGroupSessionScope') return updateGroupSessionScope;
|
||||||
|
|
|
||||||
87
src/channels/shared/default-model.mjs
Normal file
87
src/channels/shared/default-model.mjs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/** Per-bot default Session model (provider + model id), applied on session.create. */
|
||||||
|
|
||||||
|
export function normalizeDefaultModel(value) {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const slash = trimmed.indexOf('/');
|
||||||
|
if (slash <= 0 || slash === trimmed.length - 1) return null;
|
||||||
|
return {
|
||||||
|
provider: trimmed.slice(0, slash),
|
||||||
|
model: trimmed.slice(slash + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||||
|
const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
|
||||||
|
const model = typeof value.model === 'string' ? value.model.trim() : '';
|
||||||
|
if (!provider || !model) return null;
|
||||||
|
const selection = { provider, model };
|
||||||
|
if (value.reasoningEffort !== undefined) {
|
||||||
|
if (typeof value.reasoningEffort !== 'string' || !value.reasoningEffort.trim()) return null;
|
||||||
|
selection.reasoningEffort = value.reasoningEffort.trim();
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDefaultModel(value) {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
const normalized = normalizeDefaultModel(value);
|
||||||
|
if (!normalized) {
|
||||||
|
const error = new Error('默认模型无效。');
|
||||||
|
error.code = 'default-model-invalid';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultModelKey(selection) {
|
||||||
|
const normalized = normalizeDefaultModel(selection);
|
||||||
|
return normalized ? `${normalized.provider}/${normalized.model}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelCatalogItems(catalog) {
|
||||||
|
const items = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const group of Array.isArray(catalog?.groups) ? catalog.groups : []) {
|
||||||
|
const provider = typeof group?.id === 'string' ? group.id.trim() : '';
|
||||||
|
if (!provider || !Array.isArray(group.models)) continue;
|
||||||
|
for (const model of group.models) {
|
||||||
|
const modelId = typeof model?.id === 'string' ? model.id.trim() : '';
|
||||||
|
if (!modelId) continue;
|
||||||
|
const key = `${provider}/${modelId}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
const label = typeof model.name === 'string' && model.name.trim()
|
||||||
|
? model.name.trim()
|
||||||
|
: modelId;
|
||||||
|
items.push({
|
||||||
|
provider,
|
||||||
|
model: modelId,
|
||||||
|
key,
|
||||||
|
label: label === modelId ? key : `${label}(${key})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function catalogHasModel(catalog, selection) {
|
||||||
|
const normalized = normalizeDefaultModel(selection);
|
||||||
|
if (!normalized) return false;
|
||||||
|
return modelCatalogItems(catalog).some((item) => (
|
||||||
|
item.provider === normalized.provider && item.model === normalized.model
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeModelCatalog(value) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return { groups: [], failures: [], items: [] };
|
||||||
|
}
|
||||||
|
const groups = Array.isArray(value.groups) ? value.groups : [];
|
||||||
|
const failures = Array.isArray(value.failures) ? value.failures : [];
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
failures,
|
||||||
|
items: modelCatalogItems({ groups, failures }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,21 @@ const APPROVAL_REPLIES = new Map([
|
||||||
['no', 'rejected'],
|
['no', 'rejected'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const APPROVAL_PROMPT = '请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。';
|
const APPROVAL_PROMPT = '请在本聊天精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。勿到 DeepSeek Harness 网页里点,否则无人看会一直卡住。';
|
||||||
const APPROVAL_AFTER_QUESTION_PROMPT = '请先完成当前问题,再精准回复「批准」或「拒绝」。';
|
const APPROVAL_AFTER_QUESTION_PROMPT = '请先完成当前问题,再精准回复「批准」或「拒绝」。';
|
||||||
const APPROVAL_RESOLVED_TEXT = '该审批已处理,无需再次回复。';
|
const APPROVAL_RESOLVED_TEXT = '该审批已处理,无需再次回复。';
|
||||||
|
const APPROVAL_TIMEOUT_TEXT = '审批等待超时,已自动拒绝此次操作。请重新发送问题,或调整工具审批策略后重试。';
|
||||||
const RESOLVED_ROUTE_TTL_MS = 5 * 60_000;
|
const RESOLVED_ROUTE_TTL_MS = 5 * 60_000;
|
||||||
const MAX_RESOLVED_ROUTES = 2_048;
|
const MAX_RESOLVED_ROUTES = 2_048;
|
||||||
|
/** Default 5 minutes; set DSH_IM_INTERACTION_TIMEOUT_MS=0 to disable. */
|
||||||
|
const DEFAULT_INTERACTION_TIMEOUT_MS = 5 * 60_000;
|
||||||
|
|
||||||
|
function interactionTimeoutMs(value = process.env.DSH_IM_INTERACTION_TIMEOUT_MS) {
|
||||||
|
if (value === undefined || value === null || value === '') return DEFAULT_INTERACTION_TIMEOUT_MS;
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_INTERACTION_TIMEOUT_MS;
|
||||||
|
return parsed === 0 ? 0 : Math.max(5_000, Math.floor(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
function cleanText(value) {
|
function cleanText(value) {
|
||||||
return typeof value === 'string' ? value.trim() : '';
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
|
@ -103,13 +113,19 @@ function approvalOutcomeText(outcome) {
|
||||||
export class HarnessApprovalQueue {
|
export class HarnessApprovalQueue {
|
||||||
#label;
|
#label;
|
||||||
#logger;
|
#logger;
|
||||||
|
#timeoutMs;
|
||||||
#byId = new Map();
|
#byId = new Map();
|
||||||
#routes = new Map();
|
#routes = new Map();
|
||||||
#resolvedRoutes = new Map();
|
#resolvedRoutes = new Map();
|
||||||
|
|
||||||
constructor({ label = 'IM', logger = console } = {}) {
|
constructor({
|
||||||
|
label = 'IM',
|
||||||
|
logger = console,
|
||||||
|
timeoutMs = interactionTimeoutMs(),
|
||||||
|
} = {}) {
|
||||||
this.#label = label;
|
this.#label = label;
|
||||||
this.#logger = logger;
|
this.#logger = logger;
|
||||||
|
this.#timeoutMs = interactionTimeoutMs(timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPending(key) {
|
hasPending(key) {
|
||||||
|
|
@ -225,7 +241,13 @@ export class HarnessApprovalQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.recovered === true) {
|
if (interaction.recovered === true) {
|
||||||
await this.#rejectInteraction(interaction, payload);
|
const rejected = await this.#rejectInteraction(interaction, payload);
|
||||||
|
const send = context?.send;
|
||||||
|
if (rejected && typeof send === 'function') {
|
||||||
|
await send(t(
|
||||||
|
'检测到遗留的工具审批请求,已在聊天侧自动拒绝以免任务一直卡住。请重新发送你的问题。',
|
||||||
|
)).catch(() => undefined);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,11 +305,13 @@ export class HarnessApprovalQueue {
|
||||||
closedOutcome: null,
|
closedOutcome: null,
|
||||||
resolutionNotified: false,
|
resolutionNotified: false,
|
||||||
activationTask: null,
|
activationTask: null,
|
||||||
|
timeoutTimer: null,
|
||||||
};
|
};
|
||||||
this.#byId.set(approvalId, pending);
|
this.#byId.set(approvalId, pending);
|
||||||
const route = this.#routes.get(key) ?? { items: [] };
|
const route = this.#routes.get(key) ?? { items: [] };
|
||||||
route.items.push(pending);
|
route.items.push(pending);
|
||||||
this.#routes.set(key, route);
|
this.#routes.set(key, route);
|
||||||
|
this.#armTimeout(pending);
|
||||||
if (route.items[0] === pending) await this.#present(pending);
|
if (route.items[0] === pending) await this.#present(pending);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -456,6 +480,7 @@ export class HarnessApprovalQueue {
|
||||||
#remove(pending) {
|
#remove(pending) {
|
||||||
if (pending.inactive) return null;
|
if (pending.inactive) return null;
|
||||||
pending.inactive = true;
|
pending.inactive = true;
|
||||||
|
this.#clearTimeout(pending);
|
||||||
this.#rememberResolvedRoute(pending.key);
|
this.#rememberResolvedRoute(pending.key);
|
||||||
this.#byId.delete(pending.approvalId);
|
this.#byId.delete(pending.approvalId);
|
||||||
const route = this.#routes.get(pending.key);
|
const route = this.#routes.get(pending.key);
|
||||||
|
|
@ -470,6 +495,36 @@ export class HarnessApprovalQueue {
|
||||||
return wasCurrent ? route.items[0] : null;
|
return wasCurrent ? route.items[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#armTimeout(pending) {
|
||||||
|
this.#clearTimeout(pending);
|
||||||
|
if (!(this.#timeoutMs > 0)) return;
|
||||||
|
pending.timeoutTimer = setTimeout(() => {
|
||||||
|
pending.timeoutTimer = null;
|
||||||
|
if (pending.inactive || pending.submitting || pending.resolving) return;
|
||||||
|
this.#logger.warn?.(
|
||||||
|
`[dsh-im:${this.#label}] approval ${pending.approvalId} timed out after ${this.#timeoutMs}ms; auto-rejecting`,
|
||||||
|
);
|
||||||
|
void (async () => {
|
||||||
|
pending.resolutionNotified = true;
|
||||||
|
await pending.send(t(APPROVAL_TIMEOUT_TEXT)).catch(() => undefined);
|
||||||
|
if (pending.inactive || pending.submitting || pending.resolving) return;
|
||||||
|
await this.#submit(pending, 'rejected');
|
||||||
|
})().catch((error) => {
|
||||||
|
this.#logger.error?.(
|
||||||
|
`[dsh-im:${this.#label}] failed to auto-reject a timed-out approval:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, this.#timeoutMs);
|
||||||
|
pending.timeoutTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
#clearTimeout(pending) {
|
||||||
|
if (!pending?.timeoutTimer) return;
|
||||||
|
clearTimeout(pending.timeoutTimer);
|
||||||
|
pending.timeoutTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
#rememberResolvedRoute(key) {
|
#rememberResolvedRoute(key) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [routeKey, expiresAt] of this.#resolvedRoutes) {
|
for (const [routeKey, expiresAt] of this.#resolvedRoutes) {
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,11 @@ function consumeInteractionOwnership(ownership, entries) {
|
||||||
* Decide whether the current Host has a live dsh-im interaction watcher for
|
* Decide whether the current Host has a live dsh-im interaction watcher for
|
||||||
* this Session. The modern Harness adapter uses the same ownership registry
|
* this Session. The modern Harness adapter uses the same ownership registry
|
||||||
* as HarnessClient so it never steals a browser-owned question or approval.
|
* as HarnessClient so it never steals a browser-owned question or approval.
|
||||||
|
*
|
||||||
|
* Claim while an IM ask is still open — not only after `user/message` has
|
||||||
|
* flipped `active`. Waiting for `active` races tool approvals that fire before
|
||||||
|
* ownership consumption catches up, which silently delegates to DSH Web and
|
||||||
|
* leaves WhatsApp/etc. hung with no visible prompt.
|
||||||
*/
|
*/
|
||||||
export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = []) {
|
export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = []) {
|
||||||
if (!scope || !['object', 'function', 'string'].includes(typeof scope)
|
if (!scope || !['object', 'function', 'string'].includes(typeof scope)
|
||||||
|
|
@ -407,9 +412,9 @@ export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = [])
|
||||||
if (!owners || owners.size === 0) return false;
|
if (!owners || owners.size === 0) return false;
|
||||||
for (const ownership of owners) consumeInteractionOwnership(ownership, entries);
|
for (const ownership of owners) consumeInteractionOwnership(ownership, entries);
|
||||||
return [...owners].some((ownership) => (
|
return [...owners].some((ownership) => (
|
||||||
ownership.active
|
!ownership.completed
|
||||||
&& !ownership.completed
|
|
||||||
&& typeof ownership.reconnect === 'function'
|
&& typeof ownership.reconnect === 'function'
|
||||||
|
&& (ownership.active || ownership.started || ownership.promptRpcId)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1248,6 +1253,18 @@ export class HarnessClient {
|
||||||
.sort((left, right) => left.order - right.order);
|
.sort((left, right) => left.order - right.order);
|
||||||
if (active.length > 0) return { ownership: active[0], recovered: false };
|
if (active.length > 0) return { ownership: active[0], recovered: false };
|
||||||
|
|
||||||
|
// A live IM ask owns in-turn approvals/questions even before user/message
|
||||||
|
// flips `active`. Marking these recovered would auto-reject without ever
|
||||||
|
// showing the prompt in WhatsApp (or other text channels).
|
||||||
|
const live = owners
|
||||||
|
.filter((ownership) => (
|
||||||
|
!ownership.completed
|
||||||
|
&& typeof ownership.reconnect === 'function'
|
||||||
|
&& Boolean(ownership.promptRpcId)
|
||||||
|
))
|
||||||
|
.sort((left, right) => left.order - right.order)[0] ?? null;
|
||||||
|
if (live) return { ownership: live, recovered: false };
|
||||||
|
|
||||||
// A newly attached IM conversation may encounter a question left by
|
// A newly attached IM conversation may encounter a question left by
|
||||||
// an earlier runtime before its queued prompt starts. Let the oldest such
|
// an earlier runtime before its queued prompt starts. Let the oldest such
|
||||||
// ask adopt that replay so the Session can recover instead of deadlocking.
|
// ask adopt that replay so the Session can recover instead of deadlocking.
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,10 @@ export default {
|
||||||
'This conversation exceeds the model context limit. Send /compact or /new, then try again.',
|
'This conversation exceeds the model context limit. Send /compact or /new, then try again.',
|
||||||
'当前模型不存在或暂不可用。请发送 /models 查看并使用 /model 切换模型。':
|
'当前模型不存在或暂不可用。请发送 /models 查看并使用 /model 切换模型。':
|
||||||
'The current model does not exist or is unavailable. Send /models and use /model to switch models.',
|
'The current model does not exist or is unavailable. Send /models and use /model to switch models.',
|
||||||
'当前模型不存在或不支持所选配置。请发送 /models,并使用 /model 重新选择。':
|
'当前会话绑定的模型已不存在或不可用。请发送 /new 开启新会话后重试;或发送 /models,再用 /model 切换模型。':
|
||||||
'The current model does not exist or does not support the selected settings. Send /models and use /model to choose again.',
|
'The model bound to this Session no longer exists or is unavailable. Send /new to start a new Session, or send /models and use /model to switch.',
|
||||||
|
'原会话绑定的模型已失效,已自动开启新会话并重试。':
|
||||||
|
'The model bound to the previous Session was unavailable, so a new Session was started and the request was retried.',
|
||||||
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。':
|
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。':
|
||||||
'The current model does not support this content or the selected settings. Adjust the content, model, or reasoning effort and try again.',
|
'The current model does not support this content or the selected settings. Adjust the content, model, or reasoning effort and try again.',
|
||||||
'当前模型不支持所选配置。请切换模型或推理等级后重试。':
|
'当前模型不支持所选配置。请切换模型或推理等级后重试。':
|
||||||
|
|
@ -77,8 +79,8 @@ export default {
|
||||||
'The model rejected the current content. Revise the request and try again.',
|
'The model rejected the current content. Revise the request and try again.',
|
||||||
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。':
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。':
|
||||||
'The model reached its output limit without producing a displayable result. Reduce the task scope and try again.',
|
'The model reached its output limit without producing a displayable result. Reduce the task scope and try again.',
|
||||||
'任务正在等待无法在当前渠道完成的操作。请在 DeepSeek Harness 中处理后再试。':
|
'任务正在等待审批或问答,但当前聊天未能收到该请求(可能只出现在 DeepSeek Harness 网页)。请发送 /stop 取消后重试;若再次出现,请在本聊天回复「批准」或「拒绝」。':
|
||||||
'The task is waiting for an action that cannot be completed in this channel. Handle it in DeepSeek Harness, then try again.',
|
'The task is waiting for an approval or question that did not reach this chat (it may only appear in the DeepSeek Harness web UI). Send /stop to cancel and try again; if it happens again, reply 批准 / 拒绝 (or approve / reject) here in chat.',
|
||||||
'任务被意外中断,本次未完成。请重试。':
|
'任务被意外中断,本次未完成。请重试。':
|
||||||
'The task was unexpectedly interrupted and did not finish. Please try again.',
|
'The task was unexpectedly interrupted and did not finish. Please try again.',
|
||||||
'当前会话已不存在。请发送 /new 创建新会话后重试。':
|
'当前会话已不存在。请发送 /new 创建新会话后重试。':
|
||||||
|
|
@ -182,4 +184,10 @@ export default {
|
||||||
'Failed to submit the answer. Please resend your answer to the current question.',
|
'Failed to submit the answer. Please resend your answer to the current question.',
|
||||||
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。':
|
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。':
|
||||||
'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.',
|
'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.',
|
||||||
|
'请在本聊天精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。勿到 DeepSeek Harness 网页里点,否则无人看会一直卡住。':
|
||||||
|
'Reply exactly with 批准 or 拒绝 in this chat (also: 同意 / 不同意 / yes / no). Do not use the DeepSeek Harness web UI — if nobody is watching it, the task will hang.',
|
||||||
|
'审批等待超时,已自动拒绝此次操作。请重新发送问题,或调整工具审批策略后重试。':
|
||||||
|
'Approval timed out and was automatically rejected. Resend your question, or adjust the tool-approval policy and try again.',
|
||||||
|
'检测到遗留的工具审批请求,已在聊天侧自动拒绝以免任务一直卡住。请重新发送你的问题。':
|
||||||
|
'A leftover tool-approval request was detected and automatically rejected in chat so the task would not hang. Please resend your question.',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ const FAILURE_MESSAGES = Object.freeze({
|
||||||
MODEL_CONTEXT_LIMIT:
|
MODEL_CONTEXT_LIMIT:
|
||||||
'当前会话内容超过模型上下文上限。请发送 /compact 或 /new 后重试。',
|
'当前会话内容超过模型上下文上限。请发送 /compact 或 /new 后重试。',
|
||||||
MODEL_UNAVAILABLE:
|
MODEL_UNAVAILABLE:
|
||||||
'当前模型不存在或不支持所选配置。请发送 /models,并使用 /model 重新选择。',
|
'当前会话绑定的模型已不存在或不可用。请发送 /new 开启新会话后重试;或发送 /models,再用 /model 切换模型。',
|
||||||
MODEL_CONFIG:
|
MODEL_CONFIG:
|
||||||
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。',
|
'当前模型不支持这类内容或所选配置。请调整内容、模型或推理等级后重试。',
|
||||||
MODEL_TIMEOUT:
|
MODEL_TIMEOUT:
|
||||||
|
|
@ -121,7 +121,7 @@ const FAILURE_MESSAGES = Object.freeze({
|
||||||
MODEL_OUTPUT_LIMIT:
|
MODEL_OUTPUT_LIMIT:
|
||||||
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。',
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。',
|
||||||
TURN_BLOCKED:
|
TURN_BLOCKED:
|
||||||
'任务正在等待无法在当前渠道完成的操作。请在 DeepSeek Harness 中处理后再试。',
|
'任务正在等待审批或问答,但当前聊天未能收到该请求(可能只出现在 DeepSeek Harness 网页)。请发送 /stop 取消后重试;若再次出现,请在本聊天回复「批准」或「拒绝」。',
|
||||||
TURN_INTERRUPTED:
|
TURN_INTERRUPTED:
|
||||||
'任务被意外中断,本次未完成。请重试。',
|
'任务被意外中断,本次未完成。请重试。',
|
||||||
SESSION_NOT_FOUND:
|
SESSION_NOT_FOUND:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import {
|
||||||
} from './semantic/delivery.mjs';
|
} from './semantic/delivery.mjs';
|
||||||
import {
|
import {
|
||||||
channelDeliveryFailure,
|
channelDeliveryFailure,
|
||||||
|
classifyMessageFailure,
|
||||||
clearLastMessageFailure,
|
clearLastMessageFailure,
|
||||||
messageFailureText,
|
messageFailureText,
|
||||||
setLastMessageFailure,
|
setLastMessageFailure,
|
||||||
|
|
@ -732,7 +733,7 @@ export class TextHarnessBridge {
|
||||||
}));
|
}));
|
||||||
contextEnhanced = content !== originalContent;
|
contextEnhanced = content !== originalContent;
|
||||||
}
|
}
|
||||||
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
const askArgs = {
|
||||||
harness: this.#harness,
|
harness: this.#harness,
|
||||||
state: this.#state,
|
state: this.#state,
|
||||||
key: conversationKey,
|
key: conversationKey,
|
||||||
|
|
@ -770,14 +771,35 @@ export class TextHarnessBridge {
|
||||||
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
||||||
files: message.files,
|
files: message.files,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
// Session remembers provider/model. When catalog/preset defaults change, the
|
||||||
|
// stale binding surfaces as MODEL_UNAVAILABLE — recreate once so field users
|
||||||
|
// need not find the Session in DSH UI or remember /new.
|
||||||
|
let sessionResetNotice = null;
|
||||||
|
let askResult;
|
||||||
|
try {
|
||||||
|
askResult = await askInWorkspaceSession(askArgs);
|
||||||
|
} catch (error) {
|
||||||
|
const staleModel = classifyMessageFailure(error).code === 'MODEL_UNAVAILABLE'
|
||||||
|
&& typeof this.#state.clearSession === 'function';
|
||||||
|
if (!staleModel) throw error;
|
||||||
|
this.#logger.warn?.(
|
||||||
|
`[dsh-im:${this.#descriptor.key}] session model unavailable; recreating Session and retrying once`,
|
||||||
|
);
|
||||||
|
await this.#state.clearSession(conversationKey);
|
||||||
|
askResult = await askInWorkspaceSession(askArgs);
|
||||||
|
sessionResetNotice = t('原会话绑定的模型已失效,已自动开启新会话并重试。');
|
||||||
|
}
|
||||||
|
const { answer, artifacts = [] } = askResult;
|
||||||
if (batchSubmission) {
|
if (batchSubmission) {
|
||||||
this.#batches.complete(conversationKey, batchSubmission.token);
|
this.#batches.complete(conversationKey, batchSubmission.token);
|
||||||
}
|
}
|
||||||
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
|
||||||
const visibleAnswer = fileOnlyCompletion
|
const visibleAnswer = fileOnlyCompletion
|
||||||
? t(FILE_ONLY_COMPLETION_TEXT)
|
? t(FILE_ONLY_COMPLETION_TEXT)
|
||||||
: answer;
|
: sessionResetNotice && cleanText(answer)
|
||||||
|
? `${sessionResetNotice}\n\n${answer}`
|
||||||
|
: (sessionResetNotice || answer);
|
||||||
const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
|
const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
|
||||||
let textDeliveryError = null;
|
let textDeliveryError = null;
|
||||||
let textReceipt = null;
|
let textReceipt = null;
|
||||||
|
|
|
||||||
97
test/channels/shared/default-model.test.mjs
Normal file
97
test/channels/shared/default-model.test.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtemp, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
catalogHasModel,
|
||||||
|
defaultModelKey,
|
||||||
|
normalizeDefaultModel,
|
||||||
|
normalizeModelCatalog,
|
||||||
|
validateDefaultModel,
|
||||||
|
} from '../../../src/channels/shared/default-model.mjs';
|
||||||
|
import {
|
||||||
|
BotWorkspaceStore,
|
||||||
|
createBotWorkspaceScope,
|
||||||
|
} from '../../../src/channels/shared/bot-workspace-store.mjs';
|
||||||
|
|
||||||
|
test('normalizeDefaultModel accepts object and provider/model strings', () => {
|
||||||
|
assert.deepEqual(normalizeDefaultModel({ provider: 'deepseek', model: 'chat' }), {
|
||||||
|
provider: 'deepseek',
|
||||||
|
model: 'chat',
|
||||||
|
});
|
||||||
|
assert.deepEqual(normalizeDefaultModel('openai/gpt-4.1'), {
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4.1',
|
||||||
|
});
|
||||||
|
assert.equal(normalizeDefaultModel(''), null);
|
||||||
|
assert.equal(normalizeDefaultModel({ provider: '', model: 'x' }), null);
|
||||||
|
assert.equal(defaultModelKey({ provider: 'a', model: 'b' }), 'a/b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validateDefaultModel rejects junk', () => {
|
||||||
|
assert.equal(validateDefaultModel(null), null);
|
||||||
|
assert.throws(() => validateDefaultModel({ provider: 'x' }), (error) => (
|
||||||
|
error.code === 'default-model-invalid'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('model catalog helpers flatten groups', () => {
|
||||||
|
const catalog = normalizeModelCatalog({
|
||||||
|
groups: [{
|
||||||
|
id: 'deepseek',
|
||||||
|
name: 'DeepSeek',
|
||||||
|
models: [{ id: 'chat', name: 'Chat' }, { id: 'reasoner', name: 'Reasoner' }],
|
||||||
|
}],
|
||||||
|
failures: [],
|
||||||
|
});
|
||||||
|
assert.equal(catalog.items.length, 2);
|
||||||
|
assert.equal(catalogHasModel(catalog, 'deepseek/chat'), true);
|
||||||
|
assert.equal(catalogHasModel(catalog, 'deepseek/missing'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BotWorkspaceStore persists and applies default model on createSession', async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), 'dsh-im-default-model-'));
|
||||||
|
try {
|
||||||
|
const store = await new BotWorkspaceStore(join(root, 'workspaces.json'), {
|
||||||
|
defaultWorkspace: root,
|
||||||
|
}).load();
|
||||||
|
await store.ensure('bot-a');
|
||||||
|
await store.setDefaultModel('bot-a', { provider: 'deepseek', model: 'chat' });
|
||||||
|
assert.deepEqual(store.defaultModelFor('bot-a'), {
|
||||||
|
provider: 'deepseek',
|
||||||
|
model: 'chat',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selected = [];
|
||||||
|
const harness = {
|
||||||
|
async createSession() { return 'session-1'; },
|
||||||
|
async selectSessionModel(sessionId, selection) {
|
||||||
|
selected.push({ sessionId, selection });
|
||||||
|
return { selected: selection };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
async clearSessions() {},
|
||||||
|
sessionFor() { return null; },
|
||||||
|
async setSession() { return true; },
|
||||||
|
};
|
||||||
|
const scoped = createBotWorkspaceScope(harness, {
|
||||||
|
botId: 'bot-a',
|
||||||
|
workspaces: store,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
const sessionId = await scoped.harness.createSession();
|
||||||
|
assert.equal(sessionId, 'session-1');
|
||||||
|
assert.deepEqual(selected, [{
|
||||||
|
sessionId: 'session-1',
|
||||||
|
selection: { provider: 'deepseek', model: 'chat' },
|
||||||
|
}]);
|
||||||
|
|
||||||
|
await store.setDefaultModel('bot-a', null);
|
||||||
|
assert.equal(store.defaultModelFor('bot-a'), null);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -198,7 +198,7 @@ test('never submits the next FIFO approval before its operation is presented', a
|
||||||
assert.equal(sent.some(({ toolName, text }) => (
|
assert.equal(sent.some(({ toolName, text }) => (
|
||||||
toolName === 'second-tool' && text.includes('second-tool --run')
|
toolName === 'second-tool' && text.includes('second-tool --run')
|
||||||
)), true);
|
)), true);
|
||||||
assert.equal(earlyTexts[0].includes('请精准回复'), true);
|
assert.equal(earlyTexts[0].includes('精准回复'), true);
|
||||||
|
|
||||||
await queue.claimReply({
|
await queue.claimReply({
|
||||||
key: 'direct:actor-a',
|
key: 'direct:actor-a',
|
||||||
|
|
@ -237,7 +237,7 @@ test('a failed presentation cannot be followed by a blind approval', async () =>
|
||||||
}).process();
|
}).process();
|
||||||
assert.equal(responses.length, 0);
|
assert.equal(responses.length, 0);
|
||||||
assert.equal(sent.some((text) => text.includes('bash --run')), true);
|
assert.equal(sent.some((text) => text.includes('bash --run')), true);
|
||||||
assert.equal(sent.some((text) => text.includes('请精准回复')), true);
|
assert.equal(sent.some((text) => text.includes('精准回复')), true);
|
||||||
|
|
||||||
await queue.claimReply({
|
await queue.claimReply({
|
||||||
key: 'direct:actor-a',
|
key: 'direct:actor-a',
|
||||||
|
|
@ -321,7 +321,7 @@ test('resolved waits for an in-flight presentation before showing the next appro
|
||||||
assert.match(sent[0], /first-tool --run/);
|
assert.match(sent[0], /first-tool --run/);
|
||||||
assert.equal(sent[1], '已拒绝此次操作。');
|
assert.equal(sent[1], '已拒绝此次操作。');
|
||||||
assert.match(sent[2], /second-tool --run/);
|
assert.match(sent[2], /second-tool --run/);
|
||||||
assert.match(sent[3], /请精准回复/);
|
assert.match(sent[3], /精准回复/);
|
||||||
assert.equal(secondResponses.length, 0);
|
assert.equal(secondResponses.length, 0);
|
||||||
|
|
||||||
await queue.claimReply({
|
await queue.claimReply({
|
||||||
|
|
@ -539,7 +539,7 @@ test('approval decisions stay bound to the initiating actor, route, and group me
|
||||||
assert.equal(responses.length, 1);
|
assert.equal(responses.length, 1);
|
||||||
assert.equal(responses[0].value.outcome, 'allowed-once');
|
assert.equal(responses[0].value.outcome, 'allowed-once');
|
||||||
assert.equal(sent.filter((text) => text.includes('只有发起当前任务')).length, 2);
|
assert.equal(sent.filter((text) => text.includes('只有发起当前任务')).length, 2);
|
||||||
assert.equal(sent.some((text) => text.includes('请精准回复')), true);
|
assert.equal(sent.some((text) => text.includes('精准回复')), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a deferred approval reply stays silent when the approval resolves with its question unfinished', async () => {
|
test('a deferred approval reply stays silent when the approval resolves with its question unfinished', async () => {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ test('WhatsApp account card uses the unified compact channel layout', () => {
|
||||||
assert.match(markup, /检查连接/);
|
assert.match(markup, /检查连接/);
|
||||||
assert.match(markup, /移除接入/);
|
assert.match(markup, /移除接入/);
|
||||||
assert.match(markup, /class="dim-presetSelect"/);
|
assert.match(markup, /class="dim-presetSelect"/);
|
||||||
|
assert.match(markup, /默认模型/);
|
||||||
|
assert.match(markup, /Agent Preset/);
|
||||||
assert.match(markup, /aria-label="更多机器人设置"/);
|
assert.match(markup, /aria-label="更多机器人设置"/);
|
||||||
assert.doesNotMatch(markup, /仅自己模式(默认)|指定联系人模式|开放响应模式/);
|
assert.doesNotMatch(markup, /仅自己模式(默认)|指定联系人模式|开放响应模式/);
|
||||||
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
|
assert.match(markup, /role="status"[^>]*>测试消息已发送/);
|
||||||
|
|
|
||||||
|
|
@ -455,3 +455,184 @@ test('modern adapter delegates interactions when a Session exposes no readable e
|
||||||
}, () => Promise.resolve(questionAnswer));
|
}, () => Promise.resolve(questionAnswer));
|
||||||
assert.deepEqual(question, questionAnswer);
|
assert.deepEqual(question, questionAnswer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
forEachSessionApi('an approval before user/message', async (sessionApi) => {
|
||||||
|
const { events, session } = sessionFixture(sessionApi);
|
||||||
|
const eventRecord = (event) => ({ type: 'event', event });
|
||||||
|
let fixture;
|
||||||
|
let turnTask;
|
||||||
|
const append = (event) => {
|
||||||
|
events.push(event);
|
||||||
|
fixture.emit('session/event', session, event);
|
||||||
|
};
|
||||||
|
const gateway = {
|
||||||
|
async invoke(request) {
|
||||||
|
const endpoint = `${request.namespace}/${request.method}`;
|
||||||
|
if (endpoint === 'session/page') {
|
||||||
|
return { records: events.map(eventRecord), hasMore: false };
|
||||||
|
}
|
||||||
|
if (endpoint === 'session/prompt') {
|
||||||
|
turnTask = (async () => {
|
||||||
|
// Race that previously delegated to DSH Web: approval arrives while the
|
||||||
|
// IM ask is live (reconnect set) but before user/message flips `active`.
|
||||||
|
append({ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } });
|
||||||
|
append({
|
||||||
|
type: 'approval/asked', seq: 1, time: 1,
|
||||||
|
data: { id: 'approval-early', toolName: 'bash', callId: 'call-early' },
|
||||||
|
});
|
||||||
|
const outcome = await fixture.waterfall('approval/request', {
|
||||||
|
agent: { id: 'session', session },
|
||||||
|
toolName: 'bash',
|
||||||
|
callId: 'call-early',
|
||||||
|
}, () => Promise.resolve('browser-owned'));
|
||||||
|
append({
|
||||||
|
type: 'user/message', seq: 2, time: 2,
|
||||||
|
data: {
|
||||||
|
turn: 1,
|
||||||
|
source: { kind: 'user', rpcId: request.args.request.requestId },
|
||||||
|
message: { content: [] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'approval/decided', seq: 3, time: 3,
|
||||||
|
data: { id: 'approval-early', outcome },
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'assistant/message', seq: 4, time: 4,
|
||||||
|
data: { turn: 1, message: { content: [{ type: 'text', text: 'early-ok' }] } },
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'turn/end', seq: 5, time: 5,
|
||||||
|
data: { turn: 1, reason: { kind: 'completed' } },
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
return { accepted: true };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected invoke ${endpoint}`);
|
||||||
|
},
|
||||||
|
async stream(request) {
|
||||||
|
if (`${request.namespace}/${request.method}` !== 'session/follow') {
|
||||||
|
throw new Error('unexpected stream');
|
||||||
|
}
|
||||||
|
return asyncValues({
|
||||||
|
type: 'snapshot', cursor: -1, records: [], hasMore: false,
|
||||||
|
projections: { asOfSeq: -1, values: {} },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fixture = fakeContext(gateway);
|
||||||
|
const client = new HarnessClient({
|
||||||
|
...harnessConnection(fixture.ctx),
|
||||||
|
workspace: '/workspace',
|
||||||
|
rpcIdPrefix: 'early-approval',
|
||||||
|
logPrefix: 'early-approval',
|
||||||
|
});
|
||||||
|
const interactions = [];
|
||||||
|
const answer = await client.ask('session', 'approve early', {
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
onInteraction: async (interaction) => {
|
||||||
|
interactions.push(interaction);
|
||||||
|
await interaction.respond({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
sessionId: interaction.sessionId,
|
||||||
|
approvalId: interaction.payload.approvalId,
|
||||||
|
outcome: 'allowed-once',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await turnTask;
|
||||||
|
assert.equal(answer, 'early-ok');
|
||||||
|
assert.equal(interactions.length, 1);
|
||||||
|
assert.equal(interactions[0].kind, 'approval');
|
||||||
|
assert.equal(events.find((event) => event.type === 'approval/decided')?.data.outcome, 'allowed-once');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('modern adapter still claims approval when callId shape drifts but toolName matches', async () => {
|
||||||
|
const { events, session } = sessionFixture('snapshotEvents');
|
||||||
|
const eventRecord = (event) => ({ type: 'event', event });
|
||||||
|
let fixture;
|
||||||
|
let turnTask;
|
||||||
|
const append = (event) => {
|
||||||
|
events.push(event);
|
||||||
|
fixture.emit('session/event', session, event);
|
||||||
|
};
|
||||||
|
const gateway = {
|
||||||
|
async invoke(request) {
|
||||||
|
const endpoint = `${request.namespace}/${request.method}`;
|
||||||
|
if (endpoint === 'session/page') {
|
||||||
|
return { records: events.map(eventRecord), hasMore: false };
|
||||||
|
}
|
||||||
|
if (endpoint === 'session/prompt') {
|
||||||
|
const rpcId = request.args.request.requestId;
|
||||||
|
turnTask = (async () => {
|
||||||
|
append({ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } });
|
||||||
|
append({
|
||||||
|
type: 'user/message', seq: 1, time: 1,
|
||||||
|
data: { turn: 1, source: { kind: 'user', rpcId }, message: { content: [] } },
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'approval/asked', seq: 2, time: 2,
|
||||||
|
// Event omits callId while the request carries one — previously next()'d to Web.
|
||||||
|
data: { id: 'approval-drift', toolName: 'bash' },
|
||||||
|
});
|
||||||
|
const outcome = await fixture.waterfall('approval/request', {
|
||||||
|
agent: { id: 'session', session },
|
||||||
|
toolName: 'bash',
|
||||||
|
callId: 'call-drift',
|
||||||
|
}, () => Promise.resolve('browser-owned'));
|
||||||
|
append({
|
||||||
|
type: 'approval/decided', seq: 3, time: 3,
|
||||||
|
data: { id: 'approval-drift', outcome },
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'assistant/message', seq: 4, time: 4,
|
||||||
|
data: { turn: 1, message: { content: [{ type: 'text', text: 'drift-ok' }] } },
|
||||||
|
});
|
||||||
|
append({
|
||||||
|
type: 'turn/end', seq: 5, time: 5,
|
||||||
|
data: { turn: 1, reason: { kind: 'completed' } },
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
return { accepted: true };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected invoke ${endpoint}`);
|
||||||
|
},
|
||||||
|
async stream(request) {
|
||||||
|
if (`${request.namespace}/${request.method}` !== 'session/follow') {
|
||||||
|
throw new Error('unexpected stream');
|
||||||
|
}
|
||||||
|
return asyncValues({
|
||||||
|
type: 'snapshot', cursor: -1, records: [], hasMore: false,
|
||||||
|
projections: { asOfSeq: -1, values: {} },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fixture = fakeContext(gateway);
|
||||||
|
const client = new HarnessClient({
|
||||||
|
...harnessConnection(fixture.ctx),
|
||||||
|
workspace: '/workspace',
|
||||||
|
rpcIdPrefix: 'drift-approval',
|
||||||
|
logPrefix: 'drift-approval',
|
||||||
|
});
|
||||||
|
const interactions = [];
|
||||||
|
const answer = await client.ask('session', 'approve drift', {
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
onInteraction: async (interaction) => {
|
||||||
|
interactions.push(interaction);
|
||||||
|
await interaction.respond({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
sessionId: interaction.sessionId,
|
||||||
|
approvalId: interaction.payload.approvalId,
|
||||||
|
outcome: 'allowed-once',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await turnTask;
|
||||||
|
assert.equal(answer, 'drift-ok');
|
||||||
|
assert.equal(interactions.length, 1);
|
||||||
|
assert.equal(events.find((event) => event.type === 'approval/decided')?.data.outcome, 'allowed-once');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue