|
@@ -15,6 +15,8 @@ const name=ref(''), username=ref(''), kind=ref<'website'|'ssh'>('website'), orig
|
|
|
const editing=ref<CredentialSummary>(), showEditor=ref(false), taskId=ref(''), credentialId=ref(''), operation=ref<CredentialOperation>('website.login')
|
|
const editing=ref<CredentialSummary>(), showEditor=ref(false), taskId=ref(''), credentialId=ref(''), operation=ref<CredentialOperation>('website.login')
|
|
|
const sshAuth=ref<'privateKey'|'password'>('password')
|
|
const sshAuth=ref<'privateKey'|'password'>('password')
|
|
|
const botId=ref('')
|
|
const botId=ref('')
|
|
|
|
|
+const saveBotId=ref('')
|
|
|
|
|
+function approvedBots(id:string){return (state.value?.botGrants??[]).filter(g=>g.credentialRef===id).map(g=>state.value?.bots?.find(b=>b.id===g.agentId)?.name??'Bot').join('、')}
|
|
|
const authenticationChanged=computed(()=>editing.value?.target.kind==='ssh'&&kind.value==='ssh'&&sshAuth.value!==(editing.value.target.auth??'privateKey'))
|
|
const authenticationChanged=computed(()=>editing.value?.target.kind==='ssh'&&kind.value==='ssh'&&sshAuth.value!==(editing.value.target.auth??'privateKey'))
|
|
|
const secretRequired=computed(()=>!editing.value||authenticationChanged.value)
|
|
const secretRequired=computed(()=>!editing.value||authenticationChanged.value)
|
|
|
const selectedCredential=computed(()=>state.value?.entries.find(entry=>entry.id===credentialId.value))
|
|
const selectedCredential=computed(()=>state.value?.entries.find(entry=>entry.id===credentialId.value))
|
|
@@ -26,7 +28,7 @@ const inflight=new Set<AbortController>()
|
|
|
function clearSecrets(){master.value='';confirmation.value='';replacement.value='';secret.value='';passphrase.value='';contents.value='';clearPassphrase.value=false}
|
|
function clearSecrets(){master.value='';confirmation.value='';replacement.value='';secret.value='';passphrase.value='';contents.value='';clearPassphrase.value=false}
|
|
|
function failure(cause:unknown){
|
|
function failure(cause:unknown){
|
|
|
const code=cause&&typeof cause==='object'&&'code' in cause?String(cause.code):''
|
|
const code=cause&&typeof cause==='object'&&'code' in cause?String(cause.code):''
|
|
|
- return ({vault_offline:'密码保险箱连接不可用,请刷新状态或检查数据目录权限。',vault_locked:'密码保险箱已锁定,请重新手动解锁后添加凭据。',vault_unlock_failed:'主密码错误或密码库已损坏,请核对主密码。',vault_invalid_request:'请检查填写内容:主密码至少 12 位,网站使用 HTTPS 地址,SSH 地址和端口需有效。手填指纹时请使用 SHA256 格式。',vault_auth_secret_required:'更换 SSH 认证方式后,请重新填写密码或私钥。',vault_executor_not_enabled:'凭据执行器暂时不可用,请刷新状态或检查服务。',vault_host_probe_failed:'无法连接 SSH 服务器;已保存的密码不受影响,请检查主机地址和端口后重试授权。',vault_host_key_required:'首次使用时请选择确认服务器并授权任务。',vault_host_key_rejected:'服务器身份已改变,已停止连接。请核对服务器后再修改高级设置中的指纹。'} as Record<string,string>)[code]??'操作未完成,请检查连接和主密码;秘密不会回显。'
|
|
|
|
|
|
|
+ return ({vault_saved_but_not_granted:'凭据已保存,但 Bot 授权未完成。请刷新后选择 Bot 重新授权。',vault_offline:'密码保险箱连接不可用,请刷新状态或检查数据目录权限。',vault_locked:'密码保险箱已锁定,请重新手动解锁后添加凭据。',vault_unlock_failed:'主密码错误或密码库已损坏,请核对主密码。',vault_invalid_request:'请检查填写内容:主密码至少 12 位,网站使用 HTTPS 地址,SSH 地址和端口需有效。手填指纹时请使用 SHA256 格式。',vault_auth_secret_required:'更换 SSH 认证方式后,请重新填写密码或私钥。',vault_executor_not_enabled:'凭据执行器暂时不可用,请刷新状态或检查服务。',vault_host_probe_failed:'无法连接 SSH 服务器;已保存的密码不受影响,请检查主机地址和端口后重试授权。',vault_host_key_required:'首次使用时请选择确认服务器并授权任务。',vault_host_key_rejected:'服务器身份已改变,已停止连接。请核对服务器后再修改高级设置中的指纹。'} as Record<string,string>)[code]??'操作未完成,请检查连接和主密码;秘密不会回显。'
|
|
|
}
|
|
}
|
|
|
async function request<T>(path:string,method:'GET'|'POST'|'PUT'|'DELETE'='GET',body?:Record<string,JsonValue>){
|
|
async function request<T>(path:string,method:'GET'|'POST'|'PUT'|'DELETE'='GET',body?:Record<string,JsonValue>){
|
|
|
const c=new AbortController();inflight.add(c)
|
|
const c=new AbortController();inflight.add(c)
|
|
@@ -44,6 +46,7 @@ async function create(){if(master.value!==confirmation.value){error.value='两
|
|
|
async function unlock(){await action(async()=>{await request('/unlock','POST',{password:master.value,seconds:300})},state.value?.storageMode==='local'?'已解锁管理 5 分钟;已授权 Bot 的使用不受此期限影响。':'已手动解锁 5 分钟;Bot 使用凭据仍需要逐任务授权。')}
|
|
async function unlock(){await action(async()=>{await request('/unlock','POST',{password:master.value,seconds:300})},state.value?.storageMode==='local'?'已解锁管理 5 分钟;已授权 Bot 的使用不受此期限影响。':'已手动解锁 5 分钟;Bot 使用凭据仍需要逐任务授权。')}
|
|
|
async function lock(){clearSecrets();showEditor.value=false;await action(async()=>{await request('/lock','POST',{})},state.value?.storageMode==='local'?'管理界面已锁定;已授权 Bot 仍可使用。':'密码库已锁定,任务授权已撤销')}
|
|
async function lock(){clearSecrets();showEditor.value=false;await action(async()=>{await request('/lock','POST',{})},state.value?.storageMode==='local'?'管理界面已锁定;已授权 Bot 仍可使用。':'密码库已锁定,任务授权已撤销')}
|
|
|
function edit(entry?:CredentialSummary){
|
|
function edit(entry?:CredentialSummary){
|
|
|
|
|
+ saveBotId.value=''
|
|
|
clearSecrets();editing.value=entry;name.value=entry?.name??'';username.value=entry?.username??'';kind.value=entry?.target.kind??'website'
|
|
clearSecrets();editing.value=entry;name.value=entry?.name??'';username.value=entry?.username??'';kind.value=entry?.target.kind??'website'
|
|
|
origin.value=entry?.target.kind==='website'?entry.target.origin:'';host.value=entry?.target.kind==='ssh'?entry.target.host:''
|
|
origin.value=entry?.target.kind==='website'?entry.target.origin:'';host.value=entry?.target.kind==='ssh'?entry.target.host:''
|
|
|
port.value=entry?.target.kind==='ssh'?entry.target.port:22;hostKey.value=entry?.target.kind==='ssh'?entry.target.hostKey??'':''
|
|
port.value=entry?.target.kind==='ssh'?entry.target.port:22;hostKey.value=entry?.target.kind==='ssh'?entry.target.hostKey??'':''
|
|
@@ -61,14 +64,19 @@ async function save(){
|
|
|
const target:Record<string,JsonValue>=kind.value==='website'?{kind:'website',origin:origin.value}:{kind:'ssh',host:host.value,port:port.value,...(hostKey.value.trim()?{hostKey:hostKey.value.trim()}:{}),...(sshAuth.value==='password'?{auth:'password'}:{})}
|
|
const target:Record<string,JsonValue>=kind.value==='website'?{kind:'website',origin:origin.value}:{kind:'ssh',host:host.value,port:port.value,...(hostKey.value.trim()?{hostKey:hostKey.value.trim()}:{}),...(sshAuth.value==='password'?{auth:'password'}:{})}
|
|
|
const usage:Record<string,JsonValue>|undefined=!configureUse.value?undefined:kind.value==='website'?{kind:'website.form',loginPath:loginPath.value,submitPath:submitPath.value,successPath:successPath.value,...(logoutPath.value?{logoutPath:logoutPath.value}:{}),formId:formId.value,usernameName:usernameName.value,passwordName:passwordName.value,successSelector:successSelector.value}:useKind.value==='ssh.exec'?{kind:'ssh.exec',command:command.value}:useKind.value==='sftp.read'?{kind:'sftp.read',remotePath:remotePath.value,maxBytes:maxBytes.value}:{kind:'sftp.write',remotePath:remotePath.value,...(contents.value||!editing.value||editing.value.usage?.kind!=='sftp.write'?{contents:contents.value}:{})}
|
|
const usage:Record<string,JsonValue>|undefined=!configureUse.value?undefined:kind.value==='website'?{kind:'website.form',loginPath:loginPath.value,submitPath:submitPath.value,successPath:successPath.value,...(logoutPath.value?{logoutPath:logoutPath.value}:{}),formId:formId.value,usernameName:usernameName.value,passwordName:passwordName.value,successSelector:successSelector.value}:useKind.value==='ssh.exec'?{kind:'ssh.exec',command:command.value}:useKind.value==='sftp.read'?{kind:'sftp.read',remotePath:remotePath.value,maxBytes:maxBytes.value}:{kind:'sftp.write',remotePath:remotePath.value,...(contents.value||!editing.value||editing.value.usage?.kind!=='sftp.write'?{contents:contents.value}:{})}
|
|
|
const body={name:name.value,username:username.value,target,...(usage?{usage}:{}),...(secret.value?{secret:secret.value}:{}),...(kind.value==='ssh'&&sshAuth.value==='privateKey'?(clearPassphrase.value?{passphrase:''}:passphrase.value?{passphrase:passphrase.value}:{}):{}),...(editing.value?{revision:editing.value.revision}:{})}
|
|
const body={name:name.value,username:username.value,target,...(usage?{usage}:{}),...(secret.value?{secret:secret.value}:{}),...(kind.value==='ssh'&&sshAuth.value==='privateKey'?(clearPassphrase.value?{passphrase:''}:passphrase.value?{passphrase:passphrase.value}:{}):{}),...(editing.value?{revision:editing.value.revision}:{})}
|
|
|
- await action(async()=>{await request(editing.value?'/entries/'+editing.value.id:'/entries',editing.value?'PUT':'POST',body);showEditor.value=false},editing.value&&state.value?.storageMode==='local'?'凭据已保存;原 Bot 授权已撤销,请重新授权。':'凭据已加密保存,密码与私钥不回显')
|
|
|
|
|
|
|
+ const selectedBot=state.value?.storageMode==='local'&&configureUse.value?saveBotId.value:''
|
|
|
|
|
+ await action(async()=>{
|
|
|
|
|
+ const saved=await request<CredentialSummary>(editing.value?'/entries/'+editing.value.id:'/entries',editing.value?'PUT':'POST',body)
|
|
|
|
|
+ showEditor.value=false
|
|
|
|
|
+ if(selectedBot){try{await request('/bot-grant','POST',{credentialRef:saved.id,agentId:selectedBot})}catch{throw {code:'vault_saved_but_not_granted'}}}
|
|
|
|
|
+ },selectedBot?'凭据已加密保存并授权所选 Bot,返回聊天即可使用。':editing.value&&state.value?.storageMode==='local'?'凭据已保存;已授权 Bot 自动使用最新配置。':'凭据已加密保存,密码与私钥不回显')
|
|
|
}
|
|
}
|
|
|
async function remove(entry:CredentialSummary){await action(async()=>{await request('/entries/'+entry.id,'DELETE')},'凭据已删除,相关任务授权已撤销')}
|
|
async function remove(entry:CredentialSummary){await action(async()=>{await request('/entries/'+entry.id,'DELETE')},'凭据已删除,相关任务授权已撤销')}
|
|
|
async function grant(){
|
|
async function grant(){
|
|
|
const task=state.value?.tasks.find(t=>t.workId===taskId.value);if(!task)return
|
|
const task=state.value?.tasks.find(t=>t.workId===taskId.value);if(!task)return
|
|
|
await action(async()=>{await request('/leases','POST',{credentialRef:credentialId.value,agentId:task.agentId,workId:task.workId,operation:operation.value,seconds:60})},'已授予此任务 60 秒;只执行已配置的操作,密码与执行正文不回显。')
|
|
await action(async()=>{await request('/leases','POST',{credentialRef:credentialId.value,agentId:task.agentId,workId:task.workId,operation:operation.value,seconds:60})},'已授予此任务 60 秒;只执行已配置的操作,密码与执行正文不回显。')
|
|
|
}
|
|
}
|
|
|
-async function grantBot(){await action(async()=>{await request('/bot-grant','POST',{credentialRef:credentialId.value,agentId:botId.value})},'此 Bot 已获持续授权;返回聊天即可使用,直到你撤销或修改凭据。')}
|
|
|
|
|
|
|
+async function grantBot(){await action(async()=>{await request('/bot-grant','POST',{credentialRef:credentialId.value,agentId:botId.value})},'此 Bot 已获持续授权;返回聊天即可使用,直到你撤销。')}
|
|
|
function selectCredential(){const plan=selectedCredential.value?.usage;if(plan)operation.value=plan.kind==='website.form'?'website.login':plan.kind}
|
|
function selectCredential(){const plan=selectedCredential.value?.usage;if(plan)operation.value=plan.kind==='website.form'?'website.login':plan.kind}
|
|
|
function selectRequest(value:View['requests'][number]){taskId.value=value.workId;credentialId.value=value.credentialRef;operation.value=value.operation}
|
|
function selectRequest(value:View['requests'][number]){taskId.value=value.workId;credentialId.value=value.credentialRef;operation.value=value.operation}
|
|
|
function describeUse(entry?:CredentialSummary){const p=entry?.usage;if(!p)return '尚未配置,将转人工接管';if(p.kind==='website.form')return '网站表单 '+p.loginPath+' → '+p.submitPath+' → '+p.successPath+(p.logoutPath?' → 退出 '+p.logoutPath:'(远端会话按站点规则到期)');if(p.kind==='ssh.exec')return 'SSH '+p.command+',固定无参数';return p.kind==='sftp.read'?'SFTP 读取校验 '+p.remotePath+',最多 '+p.maxBytes+' 字节':'SFTP 创建新文件 '+p.remotePath+','+p.bytes+' 字节'}
|
|
function describeUse(entry?:CredentialSummary){const p=entry?.usage;if(!p)return '尚未配置,将转人工接管';if(p.kind==='website.form')return '网站表单 '+p.loginPath+' → '+p.submitPath+' → '+p.successPath+(p.logoutPath?' → 退出 '+p.logoutPath:'(远端会话按站点规则到期)');if(p.kind==='ssh.exec')return 'SSH '+p.command+',固定无参数';return p.kind==='sftp.read'?'SFTP 读取校验 '+p.remotePath+',最多 '+p.maxBytes+' 字节':'SFTP 创建新文件 '+p.remotePath+','+p.bytes+' 字节'}
|
|
@@ -113,6 +121,10 @@ onBeforeUnmount(()=>{
|
|
|
<p v-if="!state?.online">密码保险箱暂时不可用,请检查夭夭数据目录权限;已配置独立密码库时,请检查该服务的私有连接。此处不会自动安装或启动服务。</p>
|
|
<p v-if="!state?.online">密码保险箱暂时不可用,请检查夭夭数据目录权限;已配置独立密码库时,请检查该服务的私有连接。此处不会自动安装或启动服务。</p>
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
<p>{{state.unlocked?'已解锁':'已锁定'}}<span v-if="state.unlockExpiresAt"> · {{new Date(state.unlockExpiresAt).toLocaleTimeString()}} 到期</span></p>
|
|
<p>{{state.unlocked?'已解锁':'已锁定'}}<span v-if="state.unlockExpiresAt"> · {{new Date(state.unlockExpiresAt).toLocaleTimeString()}} 到期</span></p>
|
|
|
|
|
+ <div v-if="state.storageMode==='local'" class="bot-approvals">
|
|
|
|
|
+ <p v-if="!state.botGrants?.length">尚未授权任何 Bot。解锁后编辑凭据,选择允许使用的 Bot 并保存。</p>
|
|
|
|
|
+ <ul v-else><li v-for="g in state.botGrants" :key="g.credentialRef+g.agentId">{{g.name}} → {{state.bots?.find(b=>b.id===g.agentId)?.name??'Bot'}} · 已持续授权<button type="button" :disabled="busy||!state.unlocked" @click="action(async()=>{await request('/bot-revoke','POST',{credentialRef:g.credentialRef,agentId:g.agentId})},'Bot 授权已撤销')">撤销 Bot 授权</button></li></ul>
|
|
|
|
|
+ </div>
|
|
|
<form v-if="!state.initialized" @submit.prevent="create">
|
|
<form v-if="!state.initialized" @submit.prevent="create">
|
|
|
<label>主密码<input v-model="master" type="password" autocomplete="new-password" minlength="12" maxlength="1024" required :disabled="busy" /></label>
|
|
<label>主密码<input v-model="master" type="password" autocomplete="new-password" minlength="12" maxlength="1024" required :disabled="busy" /></label>
|
|
|
<label>确认主密码<input v-model="confirmation" type="password" autocomplete="new-password" minlength="12" required :disabled="busy" /></label>
|
|
<label>确认主密码<input v-model="confirmation" type="password" autocomplete="new-password" minlength="12" required :disabled="busy" /></label>
|
|
@@ -126,7 +138,7 @@ onBeforeUnmount(()=>{
|
|
|
</form>
|
|
</form>
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
<div class="actions"><button type="button" :disabled="busy" @click="lock">{{state.storageMode==='local'?'锁定管理界面':'立即锁定并撤销授权'}}</button><button type="button" :disabled="busy" @click="edit()">添加凭据</button><button type="button" :disabled="busy" @click="backup">导出加密备份</button></div>
|
|
<div class="actions"><button type="button" :disabled="busy" @click="lock">{{state.storageMode==='local'?'锁定管理界面':'立即锁定并撤销授权'}}</button><button type="button" :disabled="busy" @click="edit()">添加凭据</button><button type="button" :disabled="busy" @click="backup">导出加密备份</button></div>
|
|
|
- <ul><li v-for="entry in state.entries" :key="entry.id"><strong>{{entry.name}}</strong><p>{{entry.username}} · {{entry.target.kind==='website'?entry.target.origin:entry.target.host+':'+entry.target.port}}<span v-if="entry.target.kind==='ssh'"> · {{entry.target.auth==='password'?'密码登录':'私钥登录'}}</span></p><code>{{entry.id}}</code><div class="actions"><button type="button" :disabled="busy" @click="edit(entry)">编辑</button><button type="button" :disabled="busy" @click="remove(entry)">删除并撤权</button></div></li></ul>
|
|
|
|
|
|
|
+ <ul><li v-for="entry in state.entries" :key="entry.id"><strong>{{entry.name}}</strong><p>{{entry.username}} · {{entry.target.kind==='website'?entry.target.origin:entry.target.host+':'+entry.target.port}}<span v-if="entry.target.kind==='ssh'"> · {{entry.target.auth==='password'?'密码登录':'私钥登录'}}</span></p><p v-if="state.storageMode==='local'">{{approvedBots(entry.id)?'已持续授权:'+approvedBots(entry.id):'未授权给 Bot'}}</p><code>{{entry.id}}</code><div class="actions"><button type="button" :disabled="busy" @click="edit(entry)">编辑</button><button type="button" :disabled="busy" @click="remove(entry)">删除并撤权</button></div></li></ul>
|
|
|
<form v-if="showEditor" @submit.prevent="save">
|
|
<form v-if="showEditor" @submit.prevent="save">
|
|
|
<label>名称<input v-model="name" maxlength="120" required :disabled="busy" /></label>
|
|
<label>名称<input v-model="name" maxlength="120" required :disabled="busy" /></label>
|
|
|
<label>用户名<input v-model="username" maxlength="256" autocomplete="off" required :disabled="busy" /></label>
|
|
<label>用户名<input v-model="username" maxlength="256" autocomplete="off" required :disabled="busy" /></label>
|
|
@@ -140,7 +152,7 @@ onBeforeUnmount(()=>{
|
|
|
<details v-if="kind==='ssh'" class="advanced-connection">
|
|
<details v-if="kind==='ssh'" class="advanced-connection">
|
|
|
<summary>高级连接设置(可选)</summary>
|
|
<summary>高级连接设置(可选)</summary>
|
|
|
<label>端口<input v-model.number="port" type="number" min="1" max="65535" required :disabled="busy" /></label>
|
|
<label>端口<input v-model.number="port" type="number" min="1" max="65535" required :disabled="busy" /></label>
|
|
|
- <label>服务器指纹(可选)<input v-model="hostKey" placeholder="SHA256:…" :disabled="busy" /><small>留空即可保存。首次授权会自动获取并记住服务器指纹;已有指纹会继续校验。</small></label>
|
|
|
|
|
|
|
+ <label>服务器指纹(可选)<input v-model="hostKey" placeholder="SHA256:…" :disabled="busy" /><small>留空即可保存。首次使用会自动获取并记住服务器指纹;已有指纹会继续校验。</small></label>
|
|
|
</details>
|
|
</details>
|
|
|
<label class="checkbox-field"><input v-model="configureUse" type="checkbox" :disabled="busy||!!editing?.usage" />配置允许 Bot 执行的固定操作</label>
|
|
<label class="checkbox-field"><input v-model="configureUse" type="checkbox" :disabled="busy||!!editing?.usage" />配置允许 Bot 执行的固定操作</label>
|
|
|
<template v-if="configureUse&&kind==='website'">
|
|
<template v-if="configureUse&&kind==='website'">
|
|
@@ -155,18 +167,21 @@ onBeforeUnmount(()=>{
|
|
|
<label v-else>批准的远端绝对路径<input v-model="remotePath" required :disabled="busy" /></label>
|
|
<label v-else>批准的远端绝对路径<input v-model="remotePath" required :disabled="busy" /></label>
|
|
|
<label v-if="useKind==='sftp.read'">最大读取字节数<input v-model.number="maxBytes" type="number" min="1" max="1048576" required :disabled="busy" /></label>
|
|
<label v-if="useKind==='sftp.read'">最大读取字节数<input v-model.number="maxBytes" type="number" min="1" max="1048576" required :disabled="busy" /></label>
|
|
|
<label v-if="useKind==='sftp.write'">固定写入正文(加密保存;最多 64 KiB)<textarea v-model="contents" maxlength="65536" autocomplete="off" :disabled="busy" /><small v-if="editing?.usage?.kind==='sftp.write'">留空保留原正文,不回显。写入仅创建新文件,不覆盖;中断需人工核对。</small></label>
|
|
<label v-if="useKind==='sftp.write'">固定写入正文(加密保存;最多 64 KiB)<textarea v-model="contents" maxlength="65536" autocomplete="off" :disabled="busy" /><small v-if="editing?.usage?.kind==='sftp.write'">留空保留原正文,不回显。写入仅创建新文件,不覆盖;中断需人工核对。</small></label>
|
|
|
- <p>首次授权会记住服务器,以后身份改变就停止连接。Bot 只收到退出码或字节数和校验,收不到密码、密钥、输出正文或文件正文。</p>
|
|
|
|
|
|
|
+ <p>首次使用会记住服务器,以后身份改变就停止连接。Bot 只收到退出码或字节数和校验,收不到密码、密钥、输出正文或文件正文。</p>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-if="state.storageMode==='local'&&configureUse">
|
|
|
|
|
+ <p v-if="editing&&approvedBots(editing.id)">已持续授权:{{approvedBots(editing.id)}}。修改后继续可用,无需重新授权。</p>
|
|
|
|
|
+ <label>授权给 Bot<select v-model="saveBotId" :disabled="busy"><option value="">{{editing&&approvedBots(editing.id)?'保留已有授权':'暂不授权'}}</option><option v-for="bot in state.bots??[]" :key="bot.id" :value="bot.id">{{bot.name}}</option></select><small>选择 Bot 后保存即授权,直到你撤销。</small></label>
|
|
|
</template>
|
|
</template>
|
|
|
- <div class="actions"><button :disabled="busy">加密保存</button><button type="button" :disabled="busy" @click="showEditor=false;clearSecrets()">取消</button></div>
|
|
|
|
|
|
|
+ <div class="actions"><button :disabled="busy">{{saveBotId&&configureUse?'保存并授权 Bot':'加密保存'}}</button><button type="button" :disabled="busy" @click="showEditor=false;clearSecrets()">取消</button></div>
|
|
|
</form>
|
|
</form>
|
|
|
<form v-if="state.storageMode==='local'" @submit.prevent="grantBot">
|
|
<form v-if="state.storageMode==='local'" @submit.prevent="grantBot">
|
|
|
- <h4>授权 Bot 使用</h4><p>授权一次,之后直接使用;管理界面锁定不影响授权。修改凭据会撤销原授权。</p>
|
|
|
|
|
|
|
+ <h4>授权 Bot 使用</h4><p>授权一次,之后直接使用;管理界面锁定、修改凭据或重启服务都不影响授权。</p>
|
|
|
<label>凭据<select v-model="credentialId" required :disabled="busy" @change="selectCredential"><option disabled value="">请选择</option><option v-for="e in state.entries" :key="e.id" :value="e.id">{{e.name}}</option></select></label>
|
|
<label>凭据<select v-model="credentialId" required :disabled="busy" @change="selectCredential"><option disabled value="">请选择</option><option v-for="e in state.entries" :key="e.id" :value="e.id">{{e.name}}</option></select></label>
|
|
|
<label>Bot<select v-model="botId" required :disabled="busy"><option disabled value="">请选择</option><option v-for="bot in state.bots??[]" :key="bot.id" :value="bot.id">{{bot.name}}</option></select></label>
|
|
<label>Bot<select v-model="botId" required :disabled="busy"><option disabled value="">请选择</option><option v-for="bot in state.bots??[]" :key="bot.id" :value="bot.id">{{bot.name}}</option></select></label>
|
|
|
<p>允许操作:{{describeUse(selectedCredential)}}</p>
|
|
<p>允许操作:{{describeUse(selectedCredential)}}</p>
|
|
|
- <p v-if="firstSshUse">首次授权会自动记住服务器指纹。</p>
|
|
|
|
|
|
|
+ <p v-if="firstSshUse">首次使用会自动记住服务器指纹,授权时无需服务器在线。</p>
|
|
|
<button :disabled="busy||!botId||!selectedCredential?.usage">授权此 Bot 使用</button>
|
|
<button :disabled="busy||!botId||!selectedCredential?.usage">授权此 Bot 使用</button>
|
|
|
- <ul><li v-for="g in state.botGrants??[]" :key="g.credentialRef+g.agentId">{{g.name}} → {{state.bots?.find(b=>b.id===g.agentId)?.name??'Bot'}}<button type="button" :disabled="busy" @click="action(async()=>{await request('/bot-revoke','POST',{credentialRef:g.credentialRef,agentId:g.agentId})},'Bot 授权已撤销')">撤销 Bot 授权</button></li></ul>
|
|
|
|
|
</form>
|
|
</form>
|
|
|
<form v-else @submit.prevent="grant">
|
|
<form v-else @submit.prevent="grant">
|
|
|
<h4>本次任务授权</h4><p>先为凭据配置允许 Bot 执行的固定操作,再授权活动任务。每次只绑定一个 Bot、当前任务、执行节点和指定操作;最长 60 秒,不建立长期访问权限。</p>
|
|
<h4>本次任务授权</h4><p>先为凭据配置允许 Bot 执行的固定操作,再授权活动任务。每次只绑定一个 Bot、当前任务、执行节点和指定操作;最长 60 秒,不建立长期访问权限。</p>
|