Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/docs/en/bots-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ You can also add it to the corresponding bot entry directly (manual `bots.json`
| `defaultOncall` | The bot's default: the first new topic in a new group chat is automatically bound to oncall. `{ "enabled": true, "workingDir": "~/foo", "since": <epoch ms> }`; older groups that already existed before `since` are unaffected |
| `globalGrants` | Global conversable list (`ou_xxx`, people or bots). Can converse in any group, only `canTalk` |
| `chatGrants` | Per-group, per-user authorization `{ "oc_xxx": ["ou_yyy"] }`, only grants `canTalk`. Usually written by the `/grant` card, but can also be configured by hand |
| `messageQuota` | Message-quota switch `{ "defaultLimit": N }`: once a positive integer is configured, a `/grant` without a number applies an N-message quota; if not configured, authorization is unlimited. Only constrains talk authorization, does not affect `canOperate` |
| `messageQuota` | Message-quota override `{ "defaultLimit": N }`: once a positive integer is configured, new grant cards and Oncall both use an N-message quota. When unset, new grant cards default to 3 messages per person while Oncall remains unlimited. An explicit `/grant @user N` always uses N. Only constrains talk authorization, does not affect `canOperate` |
| `restrictGrantCommands` | When `true`, people granted only via per-user authorization (`chatGrants` / `globalGrants`) are disabled from **all slash commands** and can only have plain conversations; owner / `allowedUsers` / oncall / whole-group members are unaffected. Defaults to `false` |
| `autoGrantRequestCards` | Enabled by default. Set to `false` to stop automatically sending `/grant` request cards to the owner when an unauthorized person or external bot @mentions this bot in a group and the talk gate blocks it; the message is dropped silently instead |

Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/zh/bots-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
| `defaultOncall` | 该 bot 的默认:新群聊首条新话题自动绑定 oncall。`{ "enabled": true, "workingDir": "~/foo", "since": <epoch ms> }`;`since` 之前已存在的老群不受影响 |
| `globalGrants` | 全局可对话名单(`ou_xxx`,人或 bot)。任意群可对话,仅 `canTalk` |
| `chatGrants` | 按群的 per-user 授权 `{ "oc_xxx": ["ou_yyy"] }`,仅放行 `canTalk`。一般由 `/grant` 卡片写入,也可手配 |
| `messageQuota` | 消息额度开关 `{ "defaultLimit": N }`:配了正整数后,不带数字的 `/grant` 套用 N 条额度;不配则授权无限。仅约束 talk 授权,不影响 `canOperate` |
| `messageQuota` | 消息额度覆盖 `{ "defaultLimit": N }`:配了正整数后,新授权卡与 Oncall 都使用 N 条额度;未配置时,新授权卡默认每人 3 条,Oncall 不设额度。显式 `/grant @用户 N` 始终使用 N。仅约束 talk 授权,不影响 `canOperate` |
| `restrictGrantCommands` | `true` 时,仅靠 per-user 授权(`chatGrants` / `globalGrants`)放行的人禁用**所有斜杠命令**,只能普通对话;owner / `allowedUsers` / oncall / 整群成员不受影响。默认 `false` |
| `autoGrantRequestCards` | 默认开启。显式设为 `false` 时,群里未授权的人或外部 bot @ 本 bot 但被对话权限闸挡住时,不再自动给 owner 发 `/grant` 申请卡,改为静默丢弃 |

Expand Down
Binary file added docs/assets/pr-768/authorization-defaults.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pr-768/message-cards.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/bot-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { sanitizePerBotEnv } from './core/per-bot-env.js';
import { normalizeSubstituteMode } from './services/substitute-mode-normalize.js';
import { normalizePluginIdList } from './core/plugins/ids.js';
import { normalizeVcMeetingProfileInstructions } from './services/vc-meeting-profile-instructions.js';
import { isGrantDurationOption } from './services/grant-policy.js';
import type {
VcMeetingConsumerAgentConfig,
VcMeetingConsumerConfig,
Expand Down Expand Up @@ -1274,6 +1275,11 @@ export interface BotConfig {
* 仅约束 chatGrants / globalGrants 这类 per-user talk 授权,绝不影响 canOperate。
*/
messageQuota?: { defaultLimit?: number };
/**
* 新建 per-user 授权卡的默认有限时长(毫秒)。缺省使用产品默认 1 小时;
* 已存在授权和已经生成的 pending 卡不受后续配置变更影响。
*/
grantDefaultDurationMs?: number;
/**
* scope-aware 消息额度计数(运行时状态,随授权一起持久化进 bots.json)。
* key = `chat:${chatId}:${openId}` | `global:${openId}`,value = { limit, used }。
Expand Down Expand Up @@ -2402,6 +2408,11 @@ export function parseBotConfigsFromText(jsonText: string): BotConfig[] {
if (typeof d === 'number' && Number.isInteger(d) && d > 0) messageQuota = { defaultLimit: d };
}

// 新授权默认有效期:只接受授权卡已有的四个有限选项;非法/缺省回落产品默认 1 小时。
const grantDefaultDurationMs = isGrantDurationOption(entry.grantDefaultDurationMs)
? entry.grantDefaultDurationMs
: undefined;

// quotaState:scope-aware 计数。逐项校验 key 形如 `chat:*:*` / `global:*`,
// value 为 { limit, used } 正整数(used 允许 0)。非法项丢弃;全空 → undefined。
let quotaState: { [k: string]: { limit: number; used: number } } | undefined;
Expand Down Expand Up @@ -2603,6 +2614,7 @@ export function parseBotConfigsFromText(jsonText: string): BotConfig[] {
// 只落显式 true(undefined = 关),与 restrictGrantCommands 同款,保持 bots.json 干净。
p2pOpen: entry.p2pOpen === true || undefined,
messageQuota,
grantDefaultDurationMs,
quotaState,
grantExpiryState,
restrictGrantCommands: entry.restrictGrantCommands === true || undefined,
Expand Down
23 changes: 19 additions & 4 deletions src/core/dashboard-ipc-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3013,6 +3013,7 @@ ipcRoute('GET', '/api/bot-default-oncall', async (_req, res) => {
restrictGrantCommands: grantPrefs.restrictGrantCommands,
autoGrantRequestCards: grantPrefs.autoGrantRequestCards,
messageQuotaDefaultLimit: grantPrefs.messageQuotaDefaultLimit,
grantDefaultDurationMs: grantPrefs.grantDefaultDurationMs,
p2pMode,
skillInjection,
skillInjectionSupport,
Expand Down Expand Up @@ -3169,7 +3170,8 @@ ipcRoute('PUT', '/api/bot-summary-trigger', async (req, res) => {
// Per-bot 授权偏好。Body 任意子集:
// • restrictGrantCommands: boolean — 限制被授权人只能纯对话
// • autoGrantRequestCards: boolean — 未授权 @ 被挡住时是否发 grant 申请卡
// • messageQuotaDefaultLimit: number|null — 卡片默认额度覆盖(null = 产品默认 3 条)
// • messageQuotaDefaultLimit: number|null — 卡片/Oncall 额度覆盖(null = 卡片内置 3 条、Oncall 不限)
// • grantDefaultDurationMs: number|null — 新授权默认有限时长(null = 产品默认 1 小时)
ipcRoute('PUT', '/api/bot-grant-prefs', async (req, res) => {
if (!cachedLarkAppId) return jsonRes(res, 503, { error: 'larkAppId_not_set' });
let raw: unknown;
Expand All @@ -3179,14 +3181,27 @@ ipcRoute('PUT', '/api/bot-grant-prefs', async (req, res) => {
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
return jsonRes(res, 400, { ok: false, error: 'no_valid_fields' });
}
const body = raw as { restrictGrantCommands?: unknown; autoGrantRequestCards?: unknown; messageQuotaDefaultLimit?: unknown };
const body = raw as {
restrictGrantCommands?: unknown;
autoGrantRequestCards?: unknown;
messageQuotaDefaultLimit?: unknown;
grantDefaultDurationMs?: unknown;
};

const patch: { restrictGrantCommands?: boolean; autoGrantRequestCards?: boolean; messageQuotaDefaultLimit?: number | null } = {};
const patch: {
restrictGrantCommands?: boolean;
autoGrantRequestCards?: boolean;
messageQuotaDefaultLimit?: number | null;
grantDefaultDurationMs?: number | null;
} = {};
if (typeof body.restrictGrantCommands === 'boolean') patch.restrictGrantCommands = body.restrictGrantCommands;
if (typeof body.autoGrantRequestCards === 'boolean') patch.autoGrantRequestCards = body.autoGrantRequestCards;
// null(含 JSON null)= 关闭默认额度;number = 设定(store 内再校验正整数)。
// null(含 JSON null)= 恢复内置额度策略;number = 设定覆盖值(store 内校验 1–1000)。
if (body.messageQuotaDefaultLimit === null) patch.messageQuotaDefaultLimit = null;
else if (typeof body.messageQuotaDefaultLimit === 'number') patch.messageQuotaDefaultLimit = body.messageQuotaDefaultLimit;
// null = 恢复产品默认 1 小时;number 由 store 按卡片有限选项白名单校验。
if (body.grantDefaultDurationMs === null) patch.grantDefaultDurationMs = null;
else if (typeof body.grantDefaultDurationMs === 'number') patch.grantDefaultDurationMs = body.grantDefaultDurationMs;
if (Object.keys(patch).length === 0) return jsonRes(res, 400, { ok: false, error: 'no_valid_fields' });

const r = await grantPrefsStore.updateBotGrantPrefs(cachedLarkAppId, patch);
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4979,7 +4979,7 @@ const server = createServer(async (req, res) => {

// PUT /api/bots/:appId/grant-prefs — proxy to that bot's daemon. Body carries
// any subset of `{ restrictGrantCommands?: boolean, autoGrantRequestCards?: boolean,
// messageQuotaDefaultLimit?: number|null }`.
// messageQuotaDefaultLimit?: number|null, grantDefaultDurationMs?: number|null }`.
let mBotGrantPrefs: RegExpMatchArray | null;
if (req.method === 'PUT' && (mBotGrantPrefs = url.pathname.match(/^\/api\/bots\/([^/]+)\/grant-prefs$/))) {
const appId = decodeURIComponent(mBotGrantPrefs[1]);
Expand Down
5 changes: 5 additions & 0 deletions src/dashboard/bot-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defaultSummaryRangePrefs, summaryRangeFromLegacyContentTriggers } from
import { selectionKeyForBot } from '../setup/cli-selection.js';
import { normalizeUsageDisplay } from '../bot-registry.js';
import type { CliRuntimeConfig } from '../adapters/cli/runtime.js';
import { GRANT_DURATION_OPTIONS } from '../services/grant-policy.js';

export interface DashboardBotDescriptor {
larkAppId: string;
Expand Down Expand Up @@ -113,6 +114,10 @@ export function botDefaultsPayload(bot: DashboardBotDescriptor, j?: any, error?:
substituteMode: j?.substituteMode && typeof j.substituteMode === 'object' ? j.substituteMode : null,
restrictGrantCommands: j?.restrictGrantCommands === true,
autoGrantRequestCards: j?.autoGrantRequestCards !== false,
grantDefaultDurationMs: typeof j?.grantDefaultDurationMs === 'number'
&& GRANT_DURATION_OPTIONS.includes(j.grantDefaultDurationMs as (typeof GRANT_DURATION_OPTIONS)[number])
? j.grantDefaultDurationMs
: null,
messageQuotaDefaultLimit: typeof j?.messageQuotaDefaultLimit === 'number' ? j.messageQuotaDefaultLimit : null,
p2pMode: j?.p2pMode === 'thread' ? 'thread' : 'chat',
skillInjection: (j?.skillInjection === 'global' || j?.skillInjection === 'prompt' || j?.skillInjection === 'off') ? j.skillInjection : null,
Expand Down
Loading