koishi-plugin-new-auth 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,8 @@ It implements the first usable version described in `newauth.md`:
10
10
  - Koishi's legacy `authority` value is recorded as a suggestion, not used as the final grant;
11
11
  - policies are evaluated by `scope + role + command`;
12
12
  - guild owner/admin/member roles are separated from Bot administrator;
13
- - custom roles and role members can be managed from Koishi commands.
13
+ - custom roles and role members can be managed from Koishi commands;
14
+ - Koishi Console WebUI is available when `@koishijs/plugin-console` is installed.
14
15
 
15
16
  ## Configuration
16
17
 
@@ -67,3 +68,18 @@ guest
67
68
  ```
68
69
 
69
70
  Custom roles do not inherit from other roles. To give a custom role access to a command, add an explicit policy with `newauth.allow`.
71
+
72
+ ## WebUI
73
+
74
+ Install Koishi Console and open the `⌗ 新权限` page.
75
+
76
+ The page provides:
77
+
78
+ - role-first command policy editing;
79
+ - global and guild scope switching;
80
+ - pending command review;
81
+ - legacy `authority` suggestions;
82
+ - AI-style automatic assignment for pending commands;
83
+ - command status and guild override toggles.
84
+
85
+ The automatic assignment advisor is conservative: dangerous instance-level commands are kept for `bot-admin`, group moderation commands go to `guild-admin` and `guild-owner`, normal user commands go to `guild-member` and above, and unclear commands fall back to the legacy authority suggestion.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{defineComponent as H,ref as v,computed as c,watch as J,openBlock as o,createElementBlock as a,createElementVNode as t,toDisplayString as s,withDirectives as y,Fragment as m,renderList as x,vModelSelect as U,normalizeClass as g,createCommentVNode as G,vModelCheckbox as K,createTextVNode as T,vModelText as D}from"vue";import{store as E,send as k}from"@koishijs/client";const Q={class:"new-auth-page"},W={class:"topbar"},X={class:"top-actions"},Y=["value"],Z=["disabled"],ee=["disabled"],te={class:"tabs"},le={key:0,class:"error"},ne={key:1,class:"role-layout"},se={class:"role-list"},oe=["onClick"],ae={key:0,class:"detail"},ie={class:"detail-head"},ue={class:"check"},de={class:"filters"},re={class:"command-list"},ce={class:"command-main"},ve={class:"state-buttons"},pe=["onClick"],ge=["onClick"],he=["onClick"],_e={key:2,class:"pending-grid"},fe={class:"row-actions"},be=["disabled","onClick"],ye=["disabled","onClick"],ke={key:0,class:"empty"},Ce={key:3,class:"commands-table"},we={class:"filters"},me={class:"command-main"},xe={class:"command-controls"},Ae={class:"check"},Ie=["checked","onChange"],Ve=["value","onChange"],$e=H({__name:"new-auth",setup(C){const i=v("roles"),u=v("global"),d=v(""),p=v(""),h=v("all"),$=v(false),_=v(false),A=v(""),w=c(()=>E.newauth||{roles:[],commands:[],policies:[],scopes:[{id:"global",name:"全局默认"}],pendingCount:0}),I=c(()=>w.value.roles),P=c(()=>w.value.commands),L=c(()=>w.value.policies),B=c(()=>{var n;return(n=w.value.scopes)!=null&&n.length?w.value.scopes:[{id:"global",name:"全局默认"}]}),V=c(()=>P.value.filter(n=>n.status==="pending")),f=c(()=>I.value.find(n=>n.id===d.value));J(I,n=>{!d.value&&n.length&&(d.value=n[0].id)},{immediate:true});const M=c(()=>{const n=p.value.toLowerCase();return P.value.filter(l=>h.value!=="all"&&l.status!==h.value?false:n?[l.id,l.name,l.commandPath,l.plugin,l.description].some(e=>e==null?void 0:e.toLowerCase().includes(n)):true)}),F=c(()=>f.value?M.value.filter(n=>{var l,e;return $.value||((l=f.value)==null?void 0:l.id)==="bot-admin"||((e=f.value)==null?void 0:e.scopeType)==="global"?true:n.allowGuildOverride}):[]);function S(n){const l=L.value.find(e=>e.scope===u.value&&e.roleId===d.value&&e.commandId===n);return(l==null?void 0:l.state)||"inherit"}async function b(n){_.value=true,A.value="";try{await n}catch(l){A.value=l instanceof Error?l.message:String(l)}finally{_.value=false}}async function R(){return b((async()=>{const n=await k("newauth/getData");E.newauth=n})())}function O(n,l){return b(k("newauth/setPolicy",{scope:u.value,roleId:d.value,commandId:n,state:l}))}function N(n,l){return b(k("newauth/applySuggestion",{commandId:n,scope:u.value,mode:l}))}function q(){return b(k("newauth/autoAssignPending",{scope:u.value,mode:"advisor"}))}function z(n,l){const e=l.target;return b(k("newauth/setGuildOverride",{commandId:n.id,allowGuildOverride:e.checked}))}function j(n,l){const e=l.target;return b(k("newauth/setCommandStatus",{commandId:n.id,status:e.value}))}return(n,l)=>(o(),a("main",Q,[t("header",W,[t("div",null,[l[9]||(l[9]=t("h1",null,"新权限",-1)),t("p",null,s(I.value.length)+" 个角色 / "+s(P.value.length)+" 个指令 / "+s(V.value.length)+" 个待配置",1)]),t("div",X,[y(t("select",{"onUpdate:modelValue":l[0]||(l[0]=e=>u.value=e),class:"scope-select"},[(o(true),a(m,null,x(B.value,e=>(o(),a("option",{key:e.id,value:e.id},s(e.name),9,Y))),128))],512),[[U,u.value]]),t("button",{class:"primary",disabled:_.value||!V.value.length,onClick:q}," AI 自动分配待配置 ",8,Z),t("button",{disabled:_.value,onClick:R},"刷新",8,ee)])]),t("nav",te,[t("button",{class:g({active:i.value==="roles"}),onClick:l[1]||(l[1]=e=>i.value="roles")},"角色",2),t("button",{class:g({active:i.value==="pending"}),onClick:l[2]||(l[2]=e=>i.value="pending")},"待配置",2),t("button",{class:g({active:i.value==="commands"}),onClick:l[3]||(l[3]=e=>i.value="commands")},"指令",2)]),A.value?(o(),a("p",le,s(A.value),1)):G("v-if",true),i.value==="roles"?(o(),a("section",ne,[t("aside",se,[(o(true),a(m,null,x(I.value,e=>(o(),a("button",{key:e.id,class:g(["role-item",{active:e.id===d.value}]),onClick:r=>d.value=e.id},[t("span",null,[t("strong",null,s(e.name),1),t("small",null,s(e.builtin?"内置角色":"自定义角色")+" / "+s(e.scopeType),1)]),t("em",null,s(e.allowCount),1)],10,oe))),128))]),f.value?(o(),a("section",ae,[t("div",ie,[t("div",null,[t("h2",null,s(f.value.name),1),t("p",null,s(f.value.id),1)]),t("label",ue,[y(t("input",{type:"checkbox","onUpdate:modelValue":l[4]||(l[4]=e=>$.value=e)},null,512),[[K,$.value]]),l[10]||(l[10]=T(" 审计模式 ",-1))])]),t("div",de,[y(t("input",{"onUpdate:modelValue":l[5]||(l[5]=e=>p.value=e),placeholder:"搜索指令、插件、描述"},null,512),[[D,p.value,void 0,{trim:true}]]),y(t("select",{"onUpdate:modelValue":l[6]||(l[6]=e=>h.value=e)},[...l[11]||(l[11]=[t("option",{value:"all"},"全部状态",-1),t("option",{value:"pending"},"待配置",-1),t("option",{value:"configured"},"已配置",-1),t("option",{value:"disabled"},"已禁用",-1)])],512),[[U,h.value]])]),t("div",re,[(o(true),a(m,null,x(F.value,e=>(o(),a("article",{key:e.id,class:"command-row"},[t("div",ce,[t("strong",null,s(e.name),1),t("span",null,s(e.commandPath),1),t("small",null,s(e.plugin)+" / legacy "+s(e.legacyAuthority)+" / "+s(e.suggestion.label),1)]),t("div",ve,[t("button",{class:g({active:S(e.id)==="inherit"}),onClick:r=>O(e.id,"inherit")}," 继承 ",10,pe),t("button",{class:g({active:S(e.id)==="allow"}),onClick:r=>O(e.id,"allow")}," 开 ",10,ge),t("button",{class:g({active:S(e.id)==="deny"}),onClick:r=>O(e.id,"deny")}," 关 ",10,he)])]))),128))])])):G("v-if",true)])):i.value==="pending"?(o(),a("section",_e,[(o(true),a(m,null,x(V.value,e=>(o(),a("article",{key:e.id,class:"pending-item"},[t("div",null,[t("strong",null,s(e.name),1),t("span",null,s(e.commandPath),1),t("small",null,s(e.plugin)+" / legacy "+s(e.legacyAuthority),1)]),t("dl",null,[l[12]||(l[12]=t("dt",null,"旧建议",-1)),t("dd",null,s(e.suggestion.label),1),l[13]||(l[13]=t("dt",null,"AI 建议",-1)),t("dd",null,s(e.autoAssign.label)+":"+s(e.autoAssign.reason),1)]),t("div",fe,[t("button",{disabled:_.value,onClick:r=>N(e.id,"legacy")},"采用旧建议",8,be),t("button",{class:"primary",disabled:_.value,onClick:r=>N(e.id,"advisor")},"采用 AI 建议",8,ye)])]))),128)),V.value.length?G("v-if",true):(o(),a("p",ke,"没有待配置指令。"))])):(o(),a("section",Ce,[t("div",we,[y(t("input",{"onUpdate:modelValue":l[7]||(l[7]=e=>p.value=e),placeholder:"搜索指令、插件、描述"},null,512),[[D,p.value,void 0,{trim:true}]]),y(t("select",{"onUpdate:modelValue":l[8]||(l[8]=e=>h.value=e)},[...l[14]||(l[14]=[t("option",{value:"all"},"全部状态",-1),t("option",{value:"pending"},"待配置",-1),t("option",{value:"configured"},"已配置",-1),t("option",{value:"disabled"},"已禁用",-1)])],512),[[U,h.value]])]),(o(true),a(m,null,x(M.value,e=>(o(),a("article",{key:e.id,class:"command-row"},[t("div",me,[t("strong",null,s(e.name),1),t("span",null,s(e.id),1),t("small",null,s(e.plugin)+" / "+s(e.commandPath)+" / "+s(e.autoAssign.label),1)]),t("div",xe,[t("label",Ae,[t("input",{type:"checkbox",checked:e.allowGuildOverride,onChange:r=>z(e,r)},null,40,Ie),l[15]||(l[15]=T(" 群内自治 ",-1))]),t("select",{value:e.status,onChange:r=>j(e,r)},[...l[16]||(l[16]=[t("option",{value:"pending"},"待配置",-1),t("option",{value:"configured"},"已配置",-1),t("option",{value:"disabled"},"已禁用",-1)])],40,Ve)])]))),128))]))]))}}),Pe=(C,i)=>{const u=C.__vccOpts||C;for(const[d,p]of i)u[d]=p;return u},Se=Pe($e,[["__scopeId","data-v-b49c6f52"]]),Ge=C=>{C.page({name:"新权限",path:"/new-auth",icon:"⌗",fields:["newauth"],authority:4,component:Se})};export{Ge as default};
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .new-auth-page[data-v-b49c6f52]{min-height:100vh;background:#f6f7f4;color:#1f2523;padding:20px}.topbar[data-v-b49c6f52],.detail-head[data-v-b49c6f52],.filters[data-v-b49c6f52],.row-actions[data-v-b49c6f52],.command-controls[data-v-b49c6f52],.top-actions[data-v-b49c6f52]{display:flex;align-items:center;gap:12px}.topbar[data-v-b49c6f52]{justify-content:space-between;border-bottom:1px solid #d9ded6;padding-bottom:16px}h1[data-v-b49c6f52],h2[data-v-b49c6f52],p[data-v-b49c6f52]{margin:0}h1[data-v-b49c6f52]{font-size:26px;line-height:1.2}h2[data-v-b49c6f52]{font-size:20px}p[data-v-b49c6f52],small[data-v-b49c6f52],span[data-v-b49c6f52],dd[data-v-b49c6f52],dt[data-v-b49c6f52]{color:#63706a}button[data-v-b49c6f52],select[data-v-b49c6f52],input[data-v-b49c6f52]{border:1px solid #c7cec8;border-radius:6px;background:#fff;color:#1f2523;min-height:34px;padding:0 10px;font:inherit}button[data-v-b49c6f52]{cursor:pointer}button[data-v-b49c6f52]:disabled{cursor:not-allowed;opacity:.55}.primary[data-v-b49c6f52]{background:#285e54;border-color:#285e54;color:#fff}.tabs[data-v-b49c6f52]{display:flex;gap:6px;margin:16px 0}.tabs button.active[data-v-b49c6f52],.state-buttons button.active[data-v-b49c6f52]{background:#25312d;border-color:#25312d;color:#fff}.role-layout[data-v-b49c6f52]{display:grid;grid-template-columns:minmax(220px,280px) 1fr;gap:16px}.role-list[data-v-b49c6f52]{display:grid;gap:8px;align-content:start}.role-item[data-v-b49c6f52]{display:flex;justify-content:space-between;align-items:center;min-height:58px;text-align:left}.role-item span[data-v-b49c6f52],.command-main[data-v-b49c6f52]{display:grid;gap:3px;min-width:0}.role-item.active[data-v-b49c6f52]{border-color:#285e54;box-shadow:inset 3px 0 #285e54}.role-item em[data-v-b49c6f52]{font-style:normal;color:#8b4a2f}.detail[data-v-b49c6f52]{min-width:0}.detail-head[data-v-b49c6f52]{justify-content:space-between;margin-bottom:12px}.filters[data-v-b49c6f52]{margin-bottom:12px}.filters input[data-v-b49c6f52]{flex:1;min-width:180px}.command-list[data-v-b49c6f52],.pending-grid[data-v-b49c6f52],.commands-table[data-v-b49c6f52]{display:grid;gap:8px}.command-row[data-v-b49c6f52],.pending-item[data-v-b49c6f52]{background:#fff;border:1px solid #d9ded6;border-radius:8px;padding:12px}.command-row[data-v-b49c6f52]{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center}.command-main strong[data-v-b49c6f52],.command-main span[data-v-b49c6f52],.command-main small[data-v-b49c6f52]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.state-buttons[data-v-b49c6f52]{display:grid;grid-template-columns:repeat(3,54px);gap:6px}.pending-grid[data-v-b49c6f52]{grid-template-columns:repeat(auto-fill,minmax(300px,1fr))}.pending-item[data-v-b49c6f52]{display:grid;gap:10px}dl[data-v-b49c6f52]{display:grid;grid-template-columns:58px 1fr;gap:4px 8px;margin:0}dd[data-v-b49c6f52]{margin:0}.check[data-v-b49c6f52]{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.check input[data-v-b49c6f52]{min-height:0}.error[data-v-b49c6f52]{border:1px solid #d9a09a;background:#fff1ef;color:#8a2d22;border-radius:6px;padding:10px;margin-bottom:12px}.empty[data-v-b49c6f52]{padding:24px 0}@media (max-width: 760px){.new-auth-page[data-v-b49c6f52]{padding:12px}.topbar[data-v-b49c6f52],.detail-head[data-v-b49c6f52],.command-row[data-v-b49c6f52]{grid-template-columns:1fr;display:grid}.top-actions[data-v-b49c6f52],.filters[data-v-b49c6f52],.command-controls[data-v-b49c6f52]{flex-wrap:wrap}.role-layout[data-v-b49c6f52]{grid-template-columns:1fr}.state-buttons[data-v-b49c6f52]{grid-template-columns:repeat(3,minmax(0,1fr))}}
package/lib/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { Command, Context, Schema, Session } from 'koishi';
2
2
  import type { Argv } from 'koishi';
3
3
  export declare const name = "new-auth";
4
- export declare const inject: string[];
4
+ export declare const inject: {
5
+ required: string[];
6
+ optional: string[];
7
+ };
5
8
  type RoleType = 'builtin' | 'custom';
6
9
  type ScopeType = 'global' | 'guild';
7
10
  type CommandStatus = 'pending' | 'configured' | 'disabled';
8
11
  type PolicyState = 'inherit' | 'allow' | 'deny';
12
+ type AutoAssignKind = 'public' | 'moderation' | 'owner' | 'admin' | 'legacy';
9
13
  export interface NewAuthCommandRecord {
10
14
  id: string;
11
15
  name: string;
@@ -53,6 +57,112 @@ declare module 'koishi' {
53
57
  newauth: NewAuthService;
54
58
  }
55
59
  }
60
+ declare module '@koishijs/plugin-console' {
61
+ namespace Console {
62
+ interface Services {
63
+ newauth: NewAuthConsoleService;
64
+ }
65
+ }
66
+ interface Events {
67
+ 'newauth/getData'(): Promise<NewAuthConsoleData>;
68
+ 'newauth/setPolicy'(payload: SetPolicyPayload): Promise<{
69
+ success: true;
70
+ }>;
71
+ 'newauth/setCommandStatus'(payload: SetCommandStatusPayload): Promise<{
72
+ success: true;
73
+ }>;
74
+ 'newauth/setGuildOverride'(payload: SetGuildOverridePayload): Promise<{
75
+ success: true;
76
+ }>;
77
+ 'newauth/applySuggestion'(payload: ApplySuggestionPayload): Promise<{
78
+ success: true;
79
+ }>;
80
+ 'newauth/autoAssignPending'(payload?: AutoAssignPayload): Promise<{
81
+ success: true;
82
+ count: number;
83
+ }>;
84
+ 'newauth/createRole'(payload: CreateRolePayload): Promise<{
85
+ success: true;
86
+ }>;
87
+ 'newauth/addMember'(payload: MemberPayload): Promise<{
88
+ success: true;
89
+ }>;
90
+ 'newauth/removeMember'(payload: MemberPayload): Promise<{
91
+ success: true;
92
+ }>;
93
+ 'newauth/copyRolePolicies'(payload: CopyRolePoliciesPayload): Promise<{
94
+ success: true;
95
+ count: number;
96
+ }>;
97
+ }
98
+ }
99
+ export interface NewAuthConsoleData {
100
+ roles: RoleView[];
101
+ commands: CommandView[];
102
+ policies: NewAuthPolicyRecord[];
103
+ members: NewAuthRoleMemberRecord[];
104
+ scopes: ScopeView[];
105
+ pendingCount: number;
106
+ autoAssignAvailable: boolean;
107
+ }
108
+ export interface RoleView extends NewAuthRoleRecord {
109
+ allowCount: number;
110
+ }
111
+ export interface CommandView extends NewAuthCommandRecord {
112
+ suggestion: RoleSuggestion;
113
+ autoAssign: AutoAssignSuggestion;
114
+ }
115
+ export interface ScopeView {
116
+ id: string;
117
+ name: string;
118
+ type: ScopeType;
119
+ }
120
+ export interface RoleSuggestion {
121
+ roles: string[];
122
+ label: string;
123
+ }
124
+ export interface AutoAssignSuggestion extends RoleSuggestion {
125
+ kind: AutoAssignKind;
126
+ reason: string;
127
+ }
128
+ interface SetPolicyPayload {
129
+ scope: string;
130
+ roleId: string;
131
+ commandId: string;
132
+ state: PolicyState;
133
+ }
134
+ interface SetCommandStatusPayload {
135
+ commandId: string;
136
+ status: CommandStatus;
137
+ }
138
+ interface SetGuildOverridePayload {
139
+ commandId: string;
140
+ allowGuildOverride: boolean;
141
+ }
142
+ interface ApplySuggestionPayload {
143
+ commandId: string;
144
+ scope?: string;
145
+ mode?: 'legacy' | 'advisor';
146
+ }
147
+ interface AutoAssignPayload {
148
+ scope?: string;
149
+ mode?: 'legacy' | 'advisor';
150
+ }
151
+ interface CreateRolePayload {
152
+ id: string;
153
+ name: string;
154
+ scopeType: ScopeType;
155
+ }
156
+ interface MemberPayload {
157
+ roleId: string;
158
+ uid: string;
159
+ scope: string;
160
+ }
161
+ interface CopyRolePoliciesPayload {
162
+ sourceRoleId: string;
163
+ targetRoleId: string;
164
+ scope: string;
165
+ }
56
166
  export interface Config {
57
167
  botAdmins: string[];
58
168
  trustLegacyAuthorityAsAdmin: boolean;
@@ -128,24 +238,36 @@ export declare class NewAuthService {
128
238
  query?: string;
129
239
  }): Promise<NewAuthCommandRecord[]>;
130
240
  listRoles(): Promise<NewAuthRoleRecord[]>;
241
+ listPolicies(): Promise<NewAuthPolicyRecord[]>;
242
+ listMembers(): Promise<NewAuthRoleMemberRecord[]>;
243
+ listScopes(): Promise<ScopeView[]>;
244
+ getConsoleData(): Promise<NewAuthConsoleData>;
131
245
  addBotAdmin(uid: string): Promise<void>;
132
246
  removeBotAdmin(uid: string): Promise<void>;
133
247
  createCustomRole(id: string, name: string, scopeType?: ScopeType): Promise<void>;
134
248
  addRoleMember(roleId: string, uid: string, scope?: string): Promise<void>;
135
249
  removeRoleMember(roleId: string, uid: string, scope?: string): Promise<void>;
250
+ copyRolePolicies(sourceRoleId: string, targetRoleId: string, scope?: string): Promise<number>;
136
251
  setCommandStatus(input: string, status: CommandStatus): Promise<NewAuthCommandRecord>;
252
+ setCommandGuildOverride(input: string, allowGuildOverride: boolean): Promise<NewAuthCommandRecord>;
137
253
  setCommandPolicy(scope: string, roleId: string, input: string, state: PolicyState): Promise<NewAuthCommandRecord>;
254
+ applySuggestedPolicy(input: string, scope?: string, mode?: 'legacy' | 'advisor'): Promise<NewAuthCommandRecord>;
255
+ autoAssignPending(scope?: string, mode?: 'legacy' | 'advisor'): Promise<number>;
138
256
  getCommand(input: string): Promise<NewAuthCommandRecord>;
139
257
  private ensureBuiltinRoles;
258
+ private applyRolesToCommand;
140
259
  private createCommandRecord;
141
260
  private getDescription;
142
261
  private resolveCommandInput;
143
262
  private setPolicy;
144
263
  private getEffectivePolicy;
264
+ private getAutoAssignSuggestion;
145
265
  private ensureRoleMember;
266
+ private ensureRoleExists;
146
267
  private hasPlatformRole;
147
268
  private grantRuntimeCommandPermission;
148
269
  private getCommandList;
149
270
  private isSelfCommand;
150
271
  }
272
+ export type NewAuthConsoleService = any;
151
273
  export {};
package/lib/index.js CHANGED
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NewAuthService = exports.Config = exports.inject = exports.name = void 0;
4
4
  exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
+ const path_1 = require("path");
6
7
  exports.name = 'new-auth';
7
- exports.inject = ['database'];
8
+ exports.inject = { required: ['database'], optional: ['console'] };
8
9
  const BUILTIN_ROLES = [
9
10
  {
10
11
  id: 'bot-admin',
@@ -125,6 +126,9 @@ function apply(ctx, config) {
125
126
  return service.intercept(argv);
126
127
  });
127
128
  registerManagementCommands(ctx, config, service);
129
+ ctx.inject(['console'], (ctx) => {
130
+ registerConsole(ctx, service);
131
+ });
128
132
  }
129
133
  class NewAuthService {
130
134
  constructor(ctx, config) {
@@ -290,6 +294,62 @@ class NewAuthService {
290
294
  const records = await this.ctx.database.get('new_auth_role', {});
291
295
  return records.sort((a, b) => Number(a.builtin) - Number(b.builtin) || a.id.localeCompare(b.id));
292
296
  }
297
+ async listPolicies() {
298
+ return this.ctx.database.get('new_auth_policy', {});
299
+ }
300
+ async listMembers() {
301
+ return this.ctx.database.get('new_auth_role_member', {});
302
+ }
303
+ async listScopes() {
304
+ const scopes = [{ id: 'global', name: '全局默认', type: 'global' }];
305
+ const seen = new Set(['global']);
306
+ const channels = await this.ctx.database.get('channel', {}, ['id', 'platform', 'guildId']);
307
+ for (const channel of channels) {
308
+ const guildId = channel.guildId || channel.id;
309
+ if (!channel.platform || !guildId)
310
+ continue;
311
+ const id = `guild:${channel.platform}:${guildId}`;
312
+ if (seen.has(id))
313
+ continue;
314
+ seen.add(id);
315
+ scopes.push({
316
+ id,
317
+ name: `${channel.platform}:${guildId}`,
318
+ type: 'guild',
319
+ });
320
+ }
321
+ return scopes;
322
+ }
323
+ async getConsoleData() {
324
+ const [roles, commands, policies, members, scopes] = await Promise.all([
325
+ this.listRoles(),
326
+ this.listCommands({ all: true }),
327
+ this.listPolicies(),
328
+ this.listMembers(),
329
+ this.listScopes(),
330
+ ]);
331
+ const globalAllow = new Set(policies
332
+ .filter(policy => policy.scope === 'global' && policy.state === 'allow')
333
+ .map(policy => `${policy.roleId}:${policy.commandId}`));
334
+ return {
335
+ roles: roles.map(role => ({
336
+ ...role,
337
+ allowCount: role.id === 'bot-admin'
338
+ ? commands.filter(command => command.status !== 'disabled').length
339
+ : commands.filter(command => globalAllow.has(`${role.id}:${command.id}`)).length,
340
+ })),
341
+ commands: commands.map(command => ({
342
+ ...command,
343
+ suggestion: getLegacySuggestion(command.legacyAuthority),
344
+ autoAssign: this.getAutoAssignSuggestion(command),
345
+ })),
346
+ policies,
347
+ members,
348
+ scopes,
349
+ pendingCount: commands.filter(command => command.status === 'pending').length,
350
+ autoAssignAvailable: true,
351
+ };
352
+ }
293
353
  async addBotAdmin(uid) {
294
354
  const parsed = parseUid(uid);
295
355
  await this.ensureRoleMember('bot-admin', parsed.platform, parsed.userId, 'global');
@@ -338,6 +398,18 @@ class NewAuthService {
338
398
  scope,
339
399
  });
340
400
  }
401
+ async copyRolePolicies(sourceRoleId, targetRoleId, scope = 'global') {
402
+ await this.ensureRoleExists(sourceRoleId);
403
+ await this.ensureRoleExists(targetRoleId);
404
+ const policies = await this.ctx.database.get('new_auth_policy', {
405
+ scope,
406
+ roleId: sourceRoleId,
407
+ });
408
+ for (const policy of policies) {
409
+ await this.setPolicy(scope, targetRoleId, policy.commandId, policy.state);
410
+ }
411
+ return policies.length;
412
+ }
341
413
  async setCommandStatus(input, status) {
342
414
  const command = await this.resolveCommandInput(input);
343
415
  await this.ctx.database.set('new_auth_command', command.id, {
@@ -347,6 +419,15 @@ class NewAuthService {
347
419
  this.commandCache.delete(command.id);
348
420
  return command;
349
421
  }
422
+ async setCommandGuildOverride(input, allowGuildOverride) {
423
+ const command = await this.resolveCommandInput(input);
424
+ await this.ctx.database.set('new_auth_command', command.id, {
425
+ allowGuildOverride,
426
+ updatedAt: new Date(),
427
+ });
428
+ this.commandCache.delete(command.id);
429
+ return command;
430
+ }
350
431
  async setCommandPolicy(scope, roleId, input, state) {
351
432
  const command = await this.resolveCommandInput(input);
352
433
  await this.setPolicy(scope, roleId, command.id, state);
@@ -355,6 +436,24 @@ class NewAuthService {
355
436
  }
356
437
  return command;
357
438
  }
439
+ async applySuggestedPolicy(input, scope = 'global', mode = 'legacy') {
440
+ const command = await this.resolveCommandInput(input);
441
+ const suggestion = mode === 'advisor'
442
+ ? this.getAutoAssignSuggestion(command)
443
+ : getLegacySuggestion(command.legacyAuthority);
444
+ await this.applyRolesToCommand(scope, command.id, suggestion.roles);
445
+ if (command.status === 'pending') {
446
+ await this.setCommandStatus(command.id, 'configured');
447
+ }
448
+ return command;
449
+ }
450
+ async autoAssignPending(scope = 'global', mode = 'advisor') {
451
+ const commands = await this.listCommands({ pending: true });
452
+ for (const command of commands) {
453
+ await this.applySuggestedPolicy(command.id, scope, mode);
454
+ }
455
+ return commands.length;
456
+ }
358
457
  async getCommand(input) {
359
458
  const cached = this.commandCache.get(input);
360
459
  if (cached)
@@ -382,6 +481,15 @@ class NewAuthService {
382
481
  }
383
482
  }
384
483
  }
484
+ async applyRolesToCommand(scope, commandId, roleIds) {
485
+ const roles = await this.listRoles();
486
+ const knownRoles = new Set(roles.map(role => role.id));
487
+ for (const roleId of roleIds) {
488
+ if (!knownRoles.has(roleId))
489
+ continue;
490
+ await this.setPolicy(scope, roleId, commandId, 'allow');
491
+ }
492
+ }
385
493
  createCommandRecord(command, now) {
386
494
  const plugin = inferPlugin(command);
387
495
  const legacyAuthority = inferLegacyAuthority(command);
@@ -444,16 +552,84 @@ class NewAuthService {
444
552
  });
445
553
  return global?.state ?? 'inherit';
446
554
  }
555
+ getAutoAssignSuggestion(command) {
556
+ const text = normalizeKey([
557
+ command.commandPath,
558
+ command.name,
559
+ command.plugin,
560
+ command.description,
561
+ command.aliases.join(' '),
562
+ ].join(' '));
563
+ const dangerous = [
564
+ 'plugin', 'market', 'install', 'uninstall', 'reload', 'restart',
565
+ 'config', 'database', 'db', 'file', 'delete', 'remove', 'exec',
566
+ 'eval', 'shell', 'token', 'env', 'broadcast',
567
+ '插件', '市场', '安装', '卸载', '重载', '重启', '配置', '数据库',
568
+ '文件', '删除', '执行', '脚本', '令牌', '环境变量', '广播',
569
+ ];
570
+ if (dangerous.some(keyword => text.includes(keyword))) {
571
+ return {
572
+ kind: 'admin',
573
+ roles: ['bot-admin'],
574
+ label: '仅 Bot 管理员',
575
+ reason: '命中实例级或高危操作关键词。',
576
+ };
577
+ }
578
+ const moderation = [
579
+ 'ban', 'mute', 'kick', 'warn', 'recall', 'approve', 'blacklist',
580
+ '禁言', '踢', '封禁', '警告', '撤回', '审核', '黑名单',
581
+ ];
582
+ if (moderation.some(keyword => text.includes(keyword))) {
583
+ return {
584
+ kind: 'moderation',
585
+ roles: ['guild-admin', 'guild-owner'],
586
+ label: '群管理员和群主',
587
+ reason: '命中群管理操作关键词。',
588
+ };
589
+ }
590
+ const ownerOnly = ['welcome', 'setting', 'notice', 'announce', '欢迎', '公告', '群设置'];
591
+ if (ownerOnly.some(keyword => text.includes(keyword))) {
592
+ return {
593
+ kind: 'owner',
594
+ roles: ['guild-owner'],
595
+ label: '群主',
596
+ reason: '更适合群主进行群内自治配置。',
597
+ };
598
+ }
599
+ const publicUse = [
600
+ 'help', 'sign', 'rank', 'weather', 'search', 'music', 'play', 'query',
601
+ '帮助', '签到', '排行', '天气', '搜索', '点歌', '播放', '查询',
602
+ ];
603
+ if (publicUse.some(keyword => text.includes(keyword))) {
604
+ return {
605
+ kind: 'public',
606
+ roles: ['guild-member', 'guild-admin', 'guild-owner'],
607
+ label: '群成员及以上',
608
+ reason: '命中普通用户功能关键词。',
609
+ };
610
+ }
611
+ const fallback = getLegacySuggestion(command.legacyAuthority);
612
+ return {
613
+ kind: 'legacy',
614
+ roles: fallback.roles,
615
+ label: fallback.label,
616
+ reason: '未命中明确语义,回退到旧 authority 建议。',
617
+ };
618
+ }
447
619
  async ensureRoleMember(roleId, platform, userId, scope) {
448
- const [role] = await this.ctx.database.get('new_auth_role', { id: roleId });
449
- if (!role)
450
- throw new Error(`role not found: ${roleId}`);
620
+ await this.ensureRoleExists(roleId);
451
621
  const query = { roleId, platform, userId, scope };
452
622
  const [existing] = await this.ctx.database.get('new_auth_role_member', query);
453
623
  if (!existing) {
454
624
  await this.ctx.database.create('new_auth_role_member', { ...query, createdAt: new Date() });
455
625
  }
456
626
  }
627
+ async ensureRoleExists(roleId) {
628
+ const [role] = await this.ctx.database.get('new_auth_role', { id: roleId });
629
+ if (!role)
630
+ throw new Error(`role not found: ${roleId}`);
631
+ return role;
632
+ }
457
633
  hasPlatformRole(session, roleNames) {
458
634
  const values = new Set();
459
635
  const author = session.author;
@@ -486,6 +662,66 @@ class NewAuthService {
486
662
  }
487
663
  }
488
664
  exports.NewAuthService = NewAuthService;
665
+ function createConsoleServiceClass() {
666
+ const { DataService } = require('@koishijs/plugin-console');
667
+ return class NewAuthConsoleDataService extends DataService {
668
+ constructor(ctx) {
669
+ super(ctx, 'newauth', {
670
+ immediate: true,
671
+ authority: 4,
672
+ });
673
+ }
674
+ async get(_forced, _client) {
675
+ return this.ctx.newauth.getConsoleData();
676
+ }
677
+ };
678
+ }
679
+ function registerConsole(ctx, service) {
680
+ ctx.console.addEntry({
681
+ dev: (0, path_1.resolve)(__dirname, '../client/index.ts'),
682
+ prod: (0, path_1.resolve)(__dirname, '../dist'),
683
+ });
684
+ ctx.plugin(createConsoleServiceClass());
685
+ const ok = async (task) => {
686
+ await task;
687
+ ctx.console.refresh('newauth');
688
+ return { success: true };
689
+ };
690
+ ctx.console.addListener('newauth/getData', async () => {
691
+ return service.getConsoleData();
692
+ }, { authority: 4 });
693
+ ctx.console.addListener('newauth/setPolicy', async (payload) => {
694
+ return ok(service.setCommandPolicy(payload.scope, payload.roleId, payload.commandId, payload.state));
695
+ }, { authority: 4 });
696
+ ctx.console.addListener('newauth/setCommandStatus', async (payload) => {
697
+ return ok(service.setCommandStatus(payload.commandId, payload.status));
698
+ }, { authority: 4 });
699
+ ctx.console.addListener('newauth/setGuildOverride', async (payload) => {
700
+ return ok(service.setCommandGuildOverride(payload.commandId, payload.allowGuildOverride));
701
+ }, { authority: 4 });
702
+ ctx.console.addListener('newauth/applySuggestion', async (payload) => {
703
+ return ok(service.applySuggestedPolicy(payload.commandId, payload.scope, payload.mode));
704
+ }, { authority: 4 });
705
+ ctx.console.addListener('newauth/autoAssignPending', async (payload = {}) => {
706
+ const count = await service.autoAssignPending(payload.scope, payload.mode);
707
+ ctx.console.refresh('newauth');
708
+ return { success: true, count };
709
+ }, { authority: 4 });
710
+ ctx.console.addListener('newauth/createRole', async (payload) => {
711
+ return ok(service.createCustomRole(payload.id, payload.name, payload.scopeType));
712
+ }, { authority: 4 });
713
+ ctx.console.addListener('newauth/addMember', async (payload) => {
714
+ return ok(service.addRoleMember(payload.roleId, payload.uid, payload.scope));
715
+ }, { authority: 4 });
716
+ ctx.console.addListener('newauth/removeMember', async (payload) => {
717
+ return ok(service.removeRoleMember(payload.roleId, payload.uid, payload.scope));
718
+ }, { authority: 4 });
719
+ ctx.console.addListener('newauth/copyRolePolicies', async (payload) => {
720
+ const count = await service.copyRolePolicies(payload.sourceRoleId, payload.targetRoleId, payload.scope);
721
+ ctx.console.refresh('newauth');
722
+ return { success: true, count };
723
+ }, { authority: 4 });
724
+ }
489
725
  function registerManagementCommands(ctx, config, service) {
490
726
  const authority = config.legacyAdminAuthority;
491
727
  ctx.command('newauth', '管理新权限系统', { authority })
@@ -575,6 +811,11 @@ function registerManagementCommands(ctx, config, service) {
575
811
  await service.removeRoleMember(roleId, uid, scope);
576
812
  return `已移除成员:${roleId} ${uid} ${scope}`;
577
813
  });
814
+ ctx.command('newauth.role.copy <sourceRoleId> <targetRoleId> [scope]', '复制角色权限', { authority })
815
+ .action(async (_, sourceRoleId, targetRoleId, scope = 'global') => {
816
+ const count = await service.copyRolePolicies(sourceRoleId, targetRoleId, scope);
817
+ return `已复制 ${count} 条策略:${sourceRoleId} -> ${targetRoleId} (${scope})`;
818
+ });
578
819
  }
579
820
  function inferLegacyAuthority(command) {
580
821
  if (typeof command.config.authority === 'number')
@@ -586,6 +827,36 @@ function inferLegacyAuthority(command) {
586
827
  }
587
828
  return 1;
588
829
  }
830
+ function getLegacySuggestion(authority) {
831
+ if (authority <= 0) {
832
+ return {
833
+ roles: ['guest', 'guild-member', 'guild-admin', 'guild-owner'],
834
+ label: '访客/群成员及以上',
835
+ };
836
+ }
837
+ if (authority === 1) {
838
+ return {
839
+ roles: ['guild-member', 'guild-admin', 'guild-owner'],
840
+ label: '群成员及以上',
841
+ };
842
+ }
843
+ if (authority === 2) {
844
+ return {
845
+ roles: ['guild-admin', 'guild-owner'],
846
+ label: '群管理员及以上',
847
+ };
848
+ }
849
+ if (authority === 3) {
850
+ return {
851
+ roles: ['guild-owner'],
852
+ label: '群主',
853
+ };
854
+ }
855
+ return {
856
+ roles: ['bot-admin'],
857
+ label: '仅 Bot 管理员',
858
+ };
859
+ }
589
860
  function inferPlugin(command) {
590
861
  const source = command.caller?.scope || command.ctx?.scope;
591
862
  const plugin = source?.plugin?.name || source?.uid || source?.id || 'unknown';
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "koishi-plugin-new-auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Role and scope based command permission layer for Koishi.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "files": [
8
8
  "lib",
9
+ "dist",
9
10
  "README.md",
10
11
  "newauth.md"
11
12
  ],
12
13
  "scripts": {
13
- "build": "tsc -p tsconfig.json",
14
+ "build": "npm run build:server && npm run build:client",
15
+ "build:server": "tsc -p tsconfig.json",
16
+ "build:client": "node scripts/build-client.mjs",
14
17
  "typecheck": "tsc -p tsconfig.json --noEmit",
15
18
  "prepack": "npm run build"
16
19
  },
@@ -23,9 +26,17 @@
23
26
  ],
24
27
  "license": "MIT",
25
28
  "peerDependencies": {
29
+ "@koishijs/plugin-console": "^5.30.0",
26
30
  "koishi": "^4.18.0"
27
31
  },
32
+ "peerDependenciesMeta": {
33
+ "@koishijs/plugin-console": {
34
+ "optional": true
35
+ }
36
+ },
28
37
  "devDependencies": {
38
+ "@koishijs/client": "^5.30.11",
39
+ "@koishijs/plugin-console": "^5.30.11",
29
40
  "@types/node": "^22.0.0",
30
41
  "koishi": "4.18.11",
31
42
  "typescript": "^5.8.0"