helper

package
v1.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: GPL-3.0 Imports: 51 Imported by: 0

Documentation

Index

Constants

View Source
const MaxTransferEntries = 100_000

MaxTransferEntries 是执行资源边界,不是审批弹窗上限。目录范围只审批一次,但单次任务不能 无界物化整棵文件树;达到该值时在开始传输前明确失败。

Variables

This section is empty.

Functions

func ApplyRedisScope added in v1.15.0

func ApplyRedisScope(cfg *asset_entity.RedisConfig, scope string) error

ApplyRedisScope 把 scope 落到连接配置上:单机 / 哨兵时 scope 是库号,非空时覆盖 资产默认库;集群时 scope 是节点地址,不影响连接,由执行时的路由处理。

func ArgOptionalPartition

func ArgOptionalPartition(args map[string]any) (*int32, error)

ArgOptionalPartition parses the optional --partition flag of the "message" commands.

func CanonicalizeEtcdCommand added in v1.11.2

func CanonicalizeEtcdCommand(_ *asset_entity.Asset, command string) (string, error)

CanonicalizeEtcdCommand normalizes a model-provided command into the form used for policy matching, approval display, and audit logging: a round trip through ParseCommand + FormatCommand, which normalizes case, compound-op spelling, and flag order.

This must run before the permission check (see the ordering comment on handleExec in internal/ai/tool/tool_handlers_unified.go): a syntactically invalid command will always fail execution, so it must fail here — before an approval dialog can appear — rather than after the user has already been interrupted and approved a command that then fails to parse.

func CanonicalizeKafkaCommand added in v1.11.2

func CanonicalizeKafkaCommand(_ *asset_entity.Asset, command string) (string, error)

CanonicalizeKafkaCommand 把模型给的富命令串规范化为策略层的双 token 串 "<action> <resource>"——与已删除的 7 个 kafka_* 工具逐字节相同的形式(用户库里 存着的 CmdPolicy 与 grant 都是照那个形式写的,不能变),理由见 KafkaCommand.PolicyString 的注释。

排在权限检查之前(handleExec 的顺序,见 internal/ai/tool/tool_handlers_unified.go): 语法错误、未知 flag、缺必填 flag 都必然导致执行失败,必须在弹审批对话框之前失败, 否则用户先被打断批准一次(选 "allow all" 还会落一条常驻 grant),命令才失败。

func CanonicalizeMongoCommand added in v1.11.2

func CanonicalizeMongoCommand(asset *asset_entity.Asset, command string) (string, error)

CanonicalizeMongoCommand 把模型给的命令规范化为"真正会被执行、也应被策略匹配"的 形式:解析 + 注入资产默认库 + Render。

输出是完整命令串而不是裸 operation token:审批弹窗、策略匹配与审计看到的都是它, 而 `deleteMany logs` 与 `deleteMany logs --query='{"filter":{"level":"debug"}}'` 是完全不同的两件事(前者清空整个集合,internal/ai/helper/mongodb_helper.go 里 filter 为空时默认成 bson.D{})——只显示 "deleteMany" 的弹窗无法让用户区分。 策略侧因此改用 op 感知的 policy.MatchMongoRule,裸 op 规则照样命中富命令串。

排在权限检查之前(handleExec 的顺序,见 internal/ai/tool/tool_handlers_unified.go): 语法错误必然导致执行失败,必须在弹审批对话框之前失败。

func CanonicalizeOSSCommand added in v1.12.0

func CanonicalizeOSSCommand(_ *asset_entity.Asset, command string) (string, error)

CanonicalizeOSSCommand 把模型给的命令规范化为**规范 DSL 串**(决策 D6):审批弹窗与 audit_logs.command 展示的就是它,而策略与 grant 匹配用的是 PolicyStrings() 派生出的 两段串(OSS 一条命令可派生 1~3 条,单串承载不了)。

它同时是 exec 面唯一的输入闸门,两件事都排在权限检查之前(handleExec 的顺序是 canonicalize → precheck → 权限检查):

  • 语法错误、未知 flag、缺必填 flag、相对 --file 都必然导致执行失败,必须在弹审批 对话框之前失败,否则用户先被打断批准一次(选"始终允许"还会落一条常驻 grant), 命令才失败;
  • **策略串形状的输入被挡在这里**。permission.checkOSSPermission 靠"首词含 '.'"区分 "已派生的策略串"(cp 的端点主体、用户手写的 pattern)与"DSL 命令",因此一条 `object.read mybucket/*` 从 exec 面递进去会绕过 DSL 解析器直接参与规则匹配, 撞上一条整桶 allow 就被判成 Allow 并落成 grant,之后才在执行器里因为解析不了而失败。 ParseOSSCommand 只认 bucket / object 两个 family,`object.read` 在这里就报错。

func ClosersAsOne

func ClosersAsOne(closers []io.Closer) io.Closer

ClosersAsOne 将多个 closer 打包成单个 io.Closer(用于只接受单 closer 的 API,如 ConnCache)。

func DialAssetSSH

func DialAssetSSH(ctx context.Context, assetID int64) (*ssh.Client, func(), error)

DialAssetSSH 建立到资产的 SSH 连接,返回 client 与一次性清理函数。 底层委托给 credential_resolver.DialAssetSSH,统一支持 SOCKS5/HTTP 代理与跳板机链。 调用方必须在使用结束后调用返回的 cleanup,一并关闭 client 与跳板机链上的中间连接。

func DialSSHClient

func DialSSHClient(ctx context.Context, assetID int64) (*ssh.Client, func(), error)

DialSSHClient 创建 SSH 客户端连接,自动解析凭据、代理、跳板机链。 调用者必须调用返回的 cleanup 关闭 client 与链路资源。

func ExecCommandOnAsset added in v1.11.2

func ExecCommandOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecCommandOnAsset 是不含权限检查的纯执行入口:权限检查由调用方(统一 exec 工具的 handleExec、batch_exec 的预检)在调用之前完成。scope 对 SSH 无意义,忽略。

func ExecEtcdOnAsset added in v1.11.2

func ExecEtcdOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecEtcdOnAsset is the permission-check-free execution entry point used by the unified exec tool — the only path to etcd since the exec_etcd tool was deleted. scope is meaningless for etcd and is ignored.

func ExecK8sOnAsset added in v1.11.2

func ExecK8sOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecK8sOnAsset 是不含权限检查的纯执行入口,供统一 exec 使用。 统一 exec 的 canonicalizeK8sCommand(internal/ai/execimpl)先算一次同样的 plan,两条路径 共用同一执行体。command 是原始 kubectl 命令;scope 对 k8s 无意义,忽略。

注意:cfg 与 plan 在这里会被重新计算一次——调用方(canonicalizeK8sCommand)为了权限检查 已经算过一次 EffectiveCommand,但 ExecFunc 的签名只接受 (ctx, asset, command, scope),没有位置传入已算好的 plan,所以这里必须以同一份 BuildK8sCommandPlan 重新推导 Args。两处调用的是同一个函数,不构成第二份实现。

func ExecKafkaOnAsset added in v1.11.2

func ExecKafkaOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecKafkaOnAsset is the permission-check-free execution entry point used by the unified exec tool — the only path to Kafka now that the seven kafka_* tools and their in-handler permission checks are gone. handleExec checks permission before calling it. scope is meaningless for Kafka and is ignored — the target resource is named by the command's target position (see internal/ai/skills/kafka/SKILL.md).

分派到既有的 HandleKafka* 而不是直连 kafka_svc:那 7 个函数里的 operation 分派、 kafka_args.go 的 ~20 个 *RequestFromArgs、结果序列化都已写好且有测试,重写一遍 就是 AGENTS.md 说的第二份实现。

func ExecMongoOnAsset added in v1.11.2

func ExecMongoOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecMongoOnAsset is the permission-check-free execution entry point used by the unified exec tool — the only path to MongoDB since the exec_mongo tool was deleted. scope is meaningless for MongoDB and is ignored — the database is named by the command's --db flag (see internal/ai/skills/mongodb/SKILL.md).

func ExecOSSOnAsset added in v1.12.0

func ExecOSSOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecOSSOnAsset is the permission-check-free execution entry point used by the unified exec tool; handleExec checks permission before calling it. scope is meaningless for object storage and is ignored — the target position names bucket and key (see internal/ai/skills/oss/SKILL.md).

分派到 oss_svc 而不是直连 minio:凭据解析、连接池与分页/移动语义都在 service 层写好 且有测试,重写一遍就是 AGENTS.md 说的第二份实现。

func ExecRedisOnAsset added in v1.11.2

func ExecRedisOnAsset(ctx context.Context, asset *asset_entity.Asset, command, scope string) (string, error)

ExecRedisOnAsset 是不含权限检查的纯执行入口:权限检查由调用方(统一 exec 工具的 handleExec、batch_exec 的预检)在调用之前完成,这里只负责连接与执行。 scope:单机 / 哨兵为库号(空 = 资产默认库);集群为节点 host:port(见 ExecuteRedisRaw)。

func ExecSQLOnAsset added in v1.11.2

func ExecSQLOnAsset(ctx context.Context, asset *asset_entity.Asset, sqlText, scope string) (string, error)

ExecSQLOnAsset 是不含权限检查的纯执行入口:权限检查由调用方(统一 exec 工具的 handleExec、batch_exec 的预检)在调用之前完成,这里只负责连接与执行——两者都经资产 配置的驱动。scope 非空时覆盖资产默认配置的数据库名。

func ExecSerialOnAsset added in v1.11.2

func ExecSerialOnAsset(ctx context.Context, asset *asset_entity.Asset, command, _ string) (string, error)

ExecSerialOnAsset 是不含权限检查的纯执行入口:权限检查由统一 exec 工具的 handleExec 在调用之前完成(会话是否存在则由 PrecheckSerialSession 更早地挡下)。 scope 对串口无意义,忽略。

func ExecWithStdio

func ExecWithStdio(ctx context.Context, assetID int64, command string, stdin io.Reader, stdout, stderr io.Writer) error

ExecWithStdio 在远程服务器执行命令,直接连接 stdio(支持管道)。ctx 带有 SSHClientSet 时复用其中该资产的连接(不关闭),否则一次性拨号。

func ExecuteK8sCommandLocal added in v1.11.2

func ExecuteK8sCommandLocal(ctx context.Context, kubeconfig string, args []string) (string, error)

ExecuteK8sCommandLocal 在本机执行 kubectl(要求本机已安装 kubectl)。

func ExecuteMongoDB

func ExecuteMongoDB(ctx context.Context, client *connpool.MongoClientCloser, database, collection, operation, query string) (string, error)

ExecuteMongoDB 执行 MongoDB 操作并返回 JSON 结果

func ExecuteRedis

func ExecuteRedis(ctx context.Context, client redis.UniversalClient, command, scope string) (string, error)

ExecuteRedis 执行 Redis 命令(按空白切分)并返回 JSON 结果,scope 语义同 ExecuteRedisRaw。

func ExecuteRedisRaw

func ExecuteRedisRaw(ctx context.Context, client redis.UniversalClient, args []string, scope string) (string, error)

ExecuteRedisRaw 使用预拆分的参数执行 Redis 命令(支持含空格的值)并返回 JSON 结果。 单机 / 哨兵:库号已在建连时由 ApplyRedisScope 生效,这里直接执行。 集群:按命令路由(见 execClusterArgs),scope 是节点 host:port,结果注明执行节点。

func ExecuteSQL

func ExecuteSQL(ctx context.Context, session dbdriver.Session, sqlText string) (string, error)

ExecuteSQL 在会话上执行 SQL 并返回 JSON 结果。执行失败时原样返回驱动的错误。

func ExecuteSQLPaged

func ExecuteSQLPaged(ctx context.Context, session dbdriver.Session, sqlText string, page, pageSize int) (string, error)

ExecuteSQLPaged 在会话上分页执行 SQL 并返回 JSON 结果。page 从 0 起。怎么分页、怎么 计数由驱动决定;驱动没有分页的语句(非查询等)结果里不带 total_count。

func ExecuteSSHCommand

func ExecuteSSHCommand(ctx context.Context, assetID int64, command string) (string, error)

ExecuteSSHCommand 执行一次性 SSH 命令并返回输出(每次新建连接)

func ExecuteWithSFTP

func ExecuteWithSFTP(ctx context.Context, assetID int64, fn func(*sftp.Client) error) error

ExecuteWithSFTP 在 context 带任务级 SFTP cache 时复用该资产的会话;普通调用保持一次性 SSH+SFTP 连接语义。 ctx 取消时主动关闭底层连接以打断 fn 内部可能的 io.Copy 阻塞, 从而让 AI 停止会话能立即生效(否则大文件传输会挂住 runner.Stop)。

func HandleKafkaACL

func HandleKafkaACL(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaCluster

func HandleKafkaCluster(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaConnect

func HandleKafkaConnect(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaConsumerGroup

func HandleKafkaConsumerGroup(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaMessage

func HandleKafkaMessage(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaSchema

func HandleKafkaSchema(ctx context.Context, args map[string]any) (string, error)

func HandleKafkaTopic

func HandleKafkaTopic(ctx context.Context, args map[string]any) (string, error)

func HasGlobPattern added in v1.12.0

func HasGlobPattern(s string) bool

HasGlobPattern 是 hasGlobMeta 的导出面,供入口层按 spec §6.5 判"多源形态"。 导出而不是让调用方自己写一遍 strings.ContainsAny:判形态与展开必须用同一份元字符 定义,否则会出现"按单源审批、按多源展开"这种两边对不上的传输。

func IsExpectedCloseErr

func IsExpectedCloseErr(err error) bool

IsExpectedCloseErr 判断 SSH/网络连接关闭时的预期错误。 取消路径会主动 Close session/client 打断阻塞,随后的 defer 关闭就会返回这些错误; 归类为预期错误后,上层可以跳过 warn 日志,避免噪音。

func KafkaACLCommand

func KafkaACLCommand(operation string) (string, error)

func KafkaAlterTopicConfigRequestFromArgs

func KafkaAlterTopicConfigRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.AlterTopicConfigRequest, error)

func KafkaCheckSchemaCompatibilityRequestFromArgs

func KafkaCheckSchemaCompatibilityRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.CheckSchemaCompatibilityRequest, error)

func KafkaClusterCommand

func KafkaClusterCommand(operation string) (string, error)

KafkaClusterCommand 将 cluster family 的 operation 映射为策略命令字符串。

func KafkaConnectCommand

func KafkaConnectCommand(operation, connector string) (string, error)

func KafkaConnectorConfigRequestFromArgs

func KafkaConnectorConfigRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.ConnectorConfigRequest, error)

func KafkaConsumerGroupCommand

func KafkaConsumerGroupCommand(operation, group string) (string, error)

func KafkaCreateACLRequestFromArgs

func KafkaCreateACLRequestFromArgs(assetID int64, args map[string]any) kafka_svc.CreateACLRequest

func KafkaCreateTopicRequestFromArgs

func KafkaCreateTopicRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.CreateTopicRequest, error)

func KafkaDeleteACLRequestFromArgs

func KafkaDeleteACLRequestFromArgs(assetID int64, args map[string]any) kafka_svc.DeleteACLRequest

func KafkaDeleteRecordsRequestFromArgs

func KafkaDeleteRecordsRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.DeleteRecordsRequest, error)

func KafkaDeleteSchemaRequestFromArgs

func KafkaDeleteSchemaRequestFromArgs(assetID int64, args map[string]any) kafka_svc.DeleteSchemaRequest

func KafkaInt32SliceFromJSON

func KafkaInt32SliceFromJSON(raw string) ([]int32, error)

func KafkaListACLsRequestFromArgs

func KafkaListACLsRequestFromArgs(assetID int64, args map[string]any) kafka_svc.ListACLsRequest

func KafkaMessageCommand

func KafkaMessageCommand(operation, topic string) (string, error)

func KafkaProduceHeadersFromArgs

func KafkaProduceHeadersFromArgs(args map[string]any) ([]kafka_svc.ProduceMessageHeader, error)

func KafkaRegisterSchemaRequestFromArgs

func KafkaRegisterSchemaRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.RegisterSchemaRequest, error)

func KafkaResetConsumerGroupOffsetRequestFromArgs

func KafkaResetConsumerGroupOffsetRequestFromArgs(assetID int64, args map[string]any) (kafka_svc.ResetConsumerGroupOffsetRequest, error)

func KafkaRestartConnectorRequestFromArgs

func KafkaRestartConnectorRequestFromArgs(assetID int64, args map[string]any) kafka_svc.RestartConnectorRequest

func KafkaSchemaCommand

func KafkaSchemaCommand(operation, subject string) (string, error)

func KafkaSchemaReferencesFromJSON

func KafkaSchemaReferencesFromJSON(raw string) ([]kafka_svc.SchemaReference, error)

func KafkaStringMapFromJSON

func KafkaStringMapFromJSON(raw string) (map[string]string, error)

func KafkaTopicCommand

func KafkaTopicCommand(operation, topic string) (string, error)

func ListMongoCollections

func ListMongoCollections(ctx context.Context, client *connpool.MongoClientCloser, database string) ([]string, error)

ListMongoCollections 列出指定 MongoDB 资产指定数据库的所有集合名称。

func ListMongoDatabases

func ListMongoDatabases(ctx context.Context, client *connpool.MongoClientCloser) ([]string, error)

ListMongoDatabases 列出指定 MongoDB 资产的所有数据库名称。 v1/v2 按连接模式自动分流,逻辑(含错误信息)由两个驱动共享。

func NormalizeKafkaOperation

func NormalizeKafkaOperation(operation, fallback string) string

func NormalizeTransferPath added in v1.12.0

func NormalizeTransferPath(adapter TransferAdapter, p string) string

NormalizeTransferPath 通过适配器能力规范化端点路径;没有该能力的适配器保持原值。

func ParseTransferEndpoint added in v1.12.0

func ParseTransferEndpoint(
	ctx context.Context, s string, resolve ResolveAssetFunc,
) (asset *asset_entity.Asset, path string, err error)

ParseTransferEndpoint 解析 cp 的一端:"<asset>:<path>",AI cp 工具与 opsctl cp 共用。 只按第一个冒号切,其后整段都是该资产上的路径(路径里可以再有冒号)。

无冒号、或冒号前为空(":foo")一律是本地路径:asset 为 nil、path 是原串,且不做任何资产 查询。Windows 上带盘符的路径("C:\windows"、"C:/Users/me")同样是本地路径、不查资产: 盘符和单字母资产名写法相同,本地路径优先,单字母名称的资产在 Windows 上要用资产 ID 指定。 否则冒号后必须以 "/" 开头,或是 SSH 支持的 "~" / "~/...",才算远端路径。 home 语法在适配器接线后解析,解析出的绝对路径才进入审批与 I/O。

D15 守卫(spec §6.2 / §11):冒号后不以 "/" 开头、但前缀确实解析成了一个资产时报错, 明示正确写法 "<asset>:/<path>",而不是静默回落成本地路径——写错的远端路径不该变成一句 对着磁盘上某个字面路径的"文件不存在"。

func PrecheckSerialSession added in v1.11.2

func PrecheckSerialSession(ctx context.Context, asset *asset_entity.Asset) error

PrecheckSerialSession 是给统一 exec 注册的 permission.PrecheckFunc:把"会话是否 存在"的检查挪到权限检查之前,让统一 exec 对一个没有活跃会话的串口资产直接失败 ——不弹审批对话框。

这正是 canonicalizeK8sCommand(internal/ai/execimpl/register.go)解决的同一类问题: 该检查无副作用,必须排在 CheckForAsset(可能弹审批对话框并阻塞等待用户响应)之前, 否则用户会先被弹一次审批,批准之后命令才因为没有会话而失败。串口没有可规范化的 命令,所以是 precheck 而不是 canonicalize。

func RegisterTransferAdapter added in v1.12.0

func RegisterTransferAdapter(assetType string, a TransferAdapter)

RegisterTransferAdapter 注册某资产类型的传输适配器,由持有该协议的实现在 init() 中调用。 重复注册 panic——与 permission.RegisterExecutor 一致,注册冲突是启动期的编程错误, 不该静默覆盖。

func ResolveTransferPath added in v1.12.0

func ResolveTransferPath(
	ctx context.Context, adapter TransferAdapter, asset *asset_entity.Asset, p string,
) (string, error)

ResolveTransferPath 在审批主体生成之前解析端点路径,确保批准的路径就是实际传输的路径。

func RunSSHCommand

func RunSSHCommand(ctx context.Context, client *ssh.Client, command string) (string, error)

RunSSHCommand 在已有的 SSH 客户端上执行命令

func SameAssetCopyHint added in v1.12.0

func SameAssetCopyHint(adapter TransferAdapter, assetName string) string

func WithDatabaseCache

func WithDatabaseCache(ctx context.Context, cache *DatabaseClientCache) context.Context

WithDatabaseCache 将数据库缓存注入 context

func WithKafkaService

func WithKafkaService(ctx context.Context, svc *kafka_svc.Service) context.Context

WithKafkaService 将 Kafka 服务注入 context,让 AI handler 在同一次 AI Send 内复用 franz-go client(避免每次工具调用都重新 dial+ping)。

func WithLocalWriteGate added in v1.12.0

func WithLocalWriteGate(ctx context.Context, gate LocalWriteGate) context.Context

WithLocalWriteGate 把本地写门禁注入 ctx。由 internal/app/ai 在每次 Send 时注入同一个 LocalToolGate 实例 —— 白名单按 convID 分片,因此"本次会话允许"在两条路上是同一件事。

func WithMongoDBCache

func WithMongoDBCache(ctx context.Context, cache *MongoDBClientCache) context.Context

WithMongoDBCache 将 MongoDB 缓存注入 context

func WithRedisCache

func WithRedisCache(ctx context.Context, cache *RedisClientCache) context.Context

WithRedisCache 将 Redis 缓存注入 context

func WithSFTPClientCache added in v1.12.0

func WithSFTPClientCache(ctx context.Context, cache *SFTPClientCache) context.Context

WithSFTPClientCache 把任务级 SFTP cache 注入传输调用链。

func WithSSHCache added in v1.11.2

func WithSSHCache(ctx context.Context, cache *SSHClientCache) context.Context

WithSSHCache 将 SSH 缓存注入 context。

func WithSSHClientSet added in v1.14.0

func WithSSHClientSet(ctx context.Context, set *SSHClientSet) context.Context

WithSSHClientSet 把连接集合注入 ctx,供 ExecWithStdio 复用。

func WithSSHPool

func WithSSHPool(ctx context.Context, pool *sshpool.Pool) context.Context

WithSSHPool 将 SSH 连接池注入 context(供 connpool 隧道使用)

func WithSerialManager

func WithSerialManager(ctx context.Context, mgr serial_svc.CommandManager) context.Context

WithSerialManager 将串口管理器注入 context

func WithTransferWriteScope added in v1.12.0

func WithTransferWriteScope(ctx context.Context, scope string) context.Context

WithTransferWriteScope fixes the filesystem root that an adapter write may not escape.

Types

type AIPoolDialer

type AIPoolDialer struct{}

AIPoolDialer 实现 sshpool.PoolDialer,委托给 credential_resolver 统一 dial

func (*AIPoolDialer) DialAsset

func (d *AIPoolDialer) DialAsset(ctx context.Context, assetID int64) (*ssh.Client, []io.Closer, error)

type ApprovalTarget added in v1.12.0

type ApprovalTarget struct {
	ApprovalType string
	Subject      string
}

ApprovalTarget 是一个端点操作必须同时满足的单条授权。通常只有一条;SSH 的 `-r path` 在审批前不能探测 path 是文件还是目录,因此同时要求精确路径与子树范围。

func ApprovalSubjectsFor added in v1.12.0

func ApprovalSubjectsFor(adapter TransferAdapter, p string, dir Direction) []ApprovalTarget

ApprovalSubjectsFor 返回该端点操作全部必须授权的主体。

type ConnCache

type ConnCache[C io.Closer] = KeyedConnCache[int64, C]

ConnCache 按资产 ID 区分连接的缓存。

func NewConnCache

func NewConnCache[C io.Closer](name string) *ConnCache[C]

NewConnCache 创建按资产 ID 区分的连接缓存

type DatabaseClientCache

type DatabaseClientCache = ConnCache[dbdriver.Session]

DatabaseClientCache 在同一次 AI Send 中复用数据库会话

func NewDatabaseClientCache

func NewDatabaseClientCache() *DatabaseClientCache

NewDatabaseClientCache 创建数据库会话缓存

type Direction added in v1.12.0

type Direction int

Direction 是端点在一次传输里被使用的方向。展开(DirList)自己也是一个方向:枚举读的是 元数据而不是内容,但 `cp -r web-01:/ ./x` 能把整棵文件树的结构拖出来,所以它同样要授权。

const (
	DirRead Direction = iota
	DirWrite
	DirList
	DirReadScope
	DirWriteScope
)

type Entry added in v1.12.0

type Entry struct {
	// Path 是该端点上的完整路径 / 对象 key,直接交给 OpenRead / Write。
	Path string
	// RelPath 相对展开基点,决定它在目的端的落点:目的路径 = <dst 基点> + RelPath。
	// 基点按形态取:单个文件是它所在目录,glob 是通配前的最后一层目录,递归是源目录自身。
	// 始终以 "/" 分隔,跨端点拼接才不会带上本地平台的分隔符。
	RelPath string
	Size    int64
}

Entry 是一次展开产出的单个可传输条目。只有文件/对象,没有目录——目录由 RelPath 隐含, 目的端在写入时按需创建。

type K8sCommandPlan added in v1.11.2

type K8sCommandPlan struct {
	Args             []string
	EffectiveCommand string
}

K8sCommandPlan 是解析、补全默认 context/namespace 之后的 kubectl 调用计划。 EffectiveCommand 用于权限检查展示实际执行的命令;Args 用于实际执行。

func BuildK8sCommandPlan added in v1.11.2

func BuildK8sCommandPlan(rawCommand string, cfg *asset_entity.K8sConfig) (*K8sCommandPlan, error)

BuildK8sCommandPlan 解析一条 kubectl 命令并按需注入默认 context/namespace。 供权限检查(需要 EffectiveCommand)与 ExecK8sOnAsset(需要 Args)共用。

type KafkaCommand added in v1.11.2

type KafkaCommand struct {
	Family string
	Verb   string
	Target string
	Flags  map[string]string
}

KafkaCommand 是 kafka 富命令串的结构形式。

格式:`<family> <verb> [target] [--flags]`

family 对应已删除的 7 个 kafka_* 工具,verb 对应各工具的 operation 参数, target 是资源名(topic / group / subject / connector),flags 承载其余全部参数。 之所以让 family 占 Verb 位、verb 退到第一个 positional:cmdline 只认"第一个词是 动词"这一条词法规则,kafka 的资源维度比其他类型多一层,把 family 放在最前面 才能让 `topic delete orders` 读起来仍然是命令行的样子。

func ParseKafkaCommand added in v1.11.2

func ParseKafkaCommand(s string) (*KafkaCommand, error)

ParseKafkaCommand 解析富命令串。

func (*KafkaCommand) PolicyString added in v1.11.2

func (c *KafkaCommand) PolicyString() (string, error)

PolicyString 返回策略匹配用的 "<action> <resource>" 串。

**必须**恰好两个 token,且与已删除的 7 个 kafka_* 工具的输出逐字节相同——所以这里 直接复用现有的 Kafka*Command 函数(internal/ai/helper/kafka_command.go),不重写一遍。 理由见 TestKafkaCommand_PolicyStringIsUnchangedTwoTokenForm 的注释: splitKafkaRule 要求恰好 2 段,不满足时 MatchKafkaRule 静默返回 false, 于是 BuiltinKafkaDangerousDeny 的 deny 规则不再匹配任何命令(fail-open)。

后置条件(结果必须恰好 2 个 Fields)不是可有可无的防御性代码,别顺手删: ParseKafkaCommand 的 validateKafkaTarget 只守住"解析命令串"这一条路,而 KafkaCommand 是导出结构体,统一 exec 会从结构化的工具参数直接构造它(mongo 侧 opsctl batch 的 mongo 类型条目就是这么做的,见 cmd/opsctl/command/batch.go 的 executeBatchItem)、 完全绕开解析。这条后置条件是那条缝上唯一的守卫,而这条缝一旦破了 是静默的——不满足 2-token 时 splitKafkaRule 返回 ok=false,deny 规则一条都不匹配。 宁可在这里 fail closed 报错,也不能把一个匹配不上任何规则的串交出去。

func (*KafkaCommand) Render added in v1.11.2

func (c *KafkaCommand) Render() (string, error)

Render 是 ParseKafkaCommand 的逆函数(TestKafkaCommand_RoundTrip 锁住)。

拒绝以 "--" 开头的 Target,理由与 MongoCommand.Render 拒绝同形 Collection 完全一致 (见该函数注释):cmdline.Parse 判断 positional 还是 flag,看的是**剥掉引号之后**的 词是否以 "--" 开头(internal/ai/cmdline/cmdline.go:162),判断发生在引号信息已经 丢失之后,所以加引号救不了。ParseKafkaCommand 自己永远产不出这种 Target,但 KafkaCommand 是导出结构体,统一 exec 会用结构化的工具参数直接构造它、绕开 Parse—— 拒绝好过悄悄产出一个 Render 完语义就变了的串。 同理还要校验 flag 名:cmdline.Command.Render 明说手写字面量不受 Parse 的入口校验 保护、由调用方保证 flag 名合法,KafkaCommand.Render 就是那个调用方。mongo 靠写死 db / query 两个 flag 名绕开了这件事,kafka 接受任意 flag 名,绕不开—— `{"cfg=x": "1"}` 渲染成 `--cfg=x=1`,重新解析后是 flag "cfg" 值 "x=1"。 判定复用 cmdline.ValidFlagName,不在这里重抄一份 safeUnquotedWord。

type KeyedConnCache added in v1.15.0

type KeyedConnCache[K comparable, C io.Closer] struct {
	// contains filtered or unexported fields
}

KeyedConnCache 泛型连接缓存,在同一次 AI Send 中复用连接,按 K 区分连接 (多数类型按资产 ID;连接级参数会改变连接本身时,K 需包含该参数,如 Redis 的库号)。 并发模型:mu 保护 maps;sf 保证同一 key 并发首拨只发生一次(second waiter 拿到 first dialer 的结果),batch_exec 多条命令并发命中同资产时只握手一次。

func NewKeyedConnCache added in v1.15.0

func NewKeyedConnCache[K comparable, C io.Closer](name string) *KeyedConnCache[K, C]

NewKeyedConnCache 创建按 K 区分的连接缓存

func (*KeyedConnCache[K, C]) Close added in v1.15.0

func (c *KeyedConnCache[K, C]) Close() error

Close 关闭所有缓存的连接

func (*KeyedConnCache[K, C]) Forget added in v1.15.0

func (c *KeyedConnCache[K, C]) Forget(key K)

Forget 仅把 key 对应的连接从缓存中摘除,但不主动调用 Close。 用于 client 已被上游提前关闭(例如 RunSSHCommand 在 ctx 取消时已 Close)的场景, 避免 Remove 触发二次 Close 及伴随的预期关闭错误日志。

func (*KeyedConnCache[K, C]) GetOrDial added in v1.15.0

func (c *KeyedConnCache[K, C]) GetOrDial(key K, dial func() (C, io.Closer, error)) (C, io.Closer, error)

GetOrDial 从缓存获取连接,不存在则通过 dial 创建并缓存。 返回的 closer 始终为 nil —— 缓存命中或新拨号都由 cache 持有 closer, 调用方不需要也不应该 Close 这个连接(应通过 Remove/Forget/Close 释放)。

并发安全:同一 key 的并发首拨经 singleflight 合流为一次 dial,second waiter 直接拿到 first dialer 的 client;不同 key 完全并行。

func (*KeyedConnCache[K, C]) Remove added in v1.15.0

func (c *KeyedConnCache[K, C]) Remove(key K)

Remove 关闭并移除指定 key 的缓存连接

type ListResult added in v1.12.0

type ListResult struct {
	Entries         []Entry
	SkippedSymlinks []string
}

ListResult 是一次展开的全部产出。跳过的符号链接与 Entries 分开返回,因为它们不是可传输 条目,却必须原样报给用户——静默跳过就成了一次看起来完整、实际残缺的传输。

type LocalWriteGate added in v1.12.0

type LocalWriteGate interface {
	// CheckLocalWrites 让一批本地写路径过一次 local_write 门禁:返回 nil 即放行,
	// 非 nil 是拒绝理由(可直接回给模型)。paths 必须非空。
	CheckLocalWrites(ctx context.Context, paths []string, detail string) error
}

LocalWriteGate 是"一次本地写要先过 local_write 门禁"的接缝。

实现是 internal/ai/tool.LocalToolGate —— 模型直接调用 local_write 工具时走的就是它, 两条路共用同一份会话白名单、同一个对话框、同一套 pattern 语义。接口留在本包,是因为 被它保护的那个原语(localAdapter.Write:MkdirAll + os.Create + io.Copy)就在本包, 而 helper 不能反向 import tool。

**它不是通用的本地写检查**:本地端点照旧不产生审批项(spec §6.2)。它只补 D11 的前提 不成立的那一档 —— D11 说"本地路径由用户批准的那条命令串完全决定",而当两端都被策略/ grant 自动放行时,压根没有"用户批准的那条串",于是本地写需要另一道门。

func RequireLocalWriteGate added in v1.12.0

func RequireLocalWriteGate(ctx context.Context) (LocalWriteGate, error)

RequireLocalWriteGate 取出本地写门禁,缺失即报错 —— 不放行。

与 permission.RequireChecker 同一条理由(#249):把"注入缺失"写成放行,漏接线就等于 没有门禁。而调用方走到这里时,恰恰是"这次操作一个弹框都没有过"的场合,它是仅剩的那道门。

type MongoCommand added in v1.11.2

type MongoCommand struct {
	Op         string
	Database   string
	Collection string
	Query      string
}

MongoCommand 是 mongo 富命令串的结构形式。

格式:`<op> [collection] [--db=<database>] [--query=<json>]`

之所以让 collection 走 positional、database 走 flag:collection 是几乎每次调用 都要给的字段,正好也是最自然的 positional 用法。database 在**这一层**没有默认值: mongoFind 等执行函数(internal/ai/helper/mongodb_helper.go:212 及同类)在 database 为空时直接报错。资产的 MongoDBConfig.Database 由上一层的 resolveMongoCommand (internal/ai/helper/mongo_exec.go)注入,规范化与执行两条路径共用它——本文件只做 词法,不碰资产。

func ParseMongoCommand added in v1.11.2

func ParseMongoCommand(s string) (*MongoCommand, error)

ParseMongoCommand 解析富命令串。

func (*MongoCommand) Render added in v1.11.2

func (c *MongoCommand) Render() (string, error)

Render 是 ParseMongoCommand 的逆函数(TestMongoCommand_RoundTrip 锁住)。

拒绝以 "--" 开头的 Collection。cmdline.Parse 只看词本身(Words 已经剥掉引号 之后)是否以 "--" 开头来判断它是 positional 还是 flag (internal/ai/cmdline/cmdline.go:162)——这个判断发生在引号信息已经丢失之后, 所以给 "--db=evil" 加引号并不能让它安全地回代成一个 positional:Render 出来的 串重新解析后,"--db=evil" 依然会被当成 --db=evil 这个 flag(Collection 变成 空字符串),单独一个 "--" 会被当成格式错误的 flag("malformed flag: --")。 这属于 cmdline 引号策略"位置无关"的已知局限,在 Verb 侧也有同样的记录 (internal/ai/cmdline/cmdline.go:184-192),这里不重新设计 cmdline 的引号规则, 只在 mongo 这一层堵住它:ParseMongoCommand 产出的 Collection 永远不会以 "--" 开头(这种词在解析阶段就被当 flag 吃掉了),但 MongoCommand 是导出结构体, 统一 exec 那边会用结构化的 collection 工具参数直接构造它,绕开 Parse—— 拒绝好过悄悄产出一个 Render 完语义就变了的串。

type MongoDBClientCache

type MongoDBClientCache = ConnCache[*connpool.MongoClientCloser]

MongoDBClientCache 在同一次 AI Send 中复用 MongoDB 连接

func NewMongoDBClientCache

func NewMongoDBClientCache() *MongoDBClientCache

NewMongoDBClientCache 创建 MongoDB 连接缓存

type OSSCommand added in v1.12.0

type OSSCommand struct {
	Family string
	Verb   string
	Target string
	Flags  map[string]string
}

OSSCommand 是对象存储富命令串的结构形式。

格式:`<family> <verb> [target] [--flags]`,与 KafkaCommand 同形(family 占 Verb 位、 verb 退到第一个 positional),因为 cmdline 只认"第一个词是动词"这一条词法规则, 而对象存储同样有 bucket / object 两层资源维度。

Target 是 `<bucket>[/<key-or-prefix>]`;OSS 侧一条命令可能派生多条策略串 (copy 两条、move 三条),所以对外是 PolicyStrings 而不是 kafka 的单串 PolicyString。

func ParseOSSCommand added in v1.12.0

func ParseOSSCommand(s string) (*OSSCommand, error)

ParseOSSCommand 解析富命令串。

func (*OSSCommand) PolicyStrings added in v1.12.0

func (c *OSSCommand) PolicyStrings() ([]string, error)

PolicyStrings 返回策略匹配用的 "<action> <resource>" 串,每条恰好两段。

copy / move 派生多条是本设计的核心安全点(决策 D7):只检查目的地就等于放行 "把受限对象复制到可读位置再读"的绕过路径。调用方要按"任一条被 deny 即整条命令被拒、 allow 名单存在时必须每条都命中才放行"来消费这个切片。

后置条件(每条都能被"第一段空白"切成两段)不是可有可无的防御性代码,别顺手删: ParseOSSCommand 的 target 校验只守住"解析命令串"这一条路,而 OSSCommand 是导出结构体, 统一 exec / opsctl batch 会从结构化参数直接构造它、完全绕开解析。这条后置条件是那条缝上 唯一的守卫,而这条缝一旦破了是**静默**的——切不出两段时匹配器对任何规则都返回 false, deny 规则一条都不匹配(fail-open),allow 名单侧则永远不放行。宁可在这里 fail closed 报错,也不能把一个匹配不上任何规则的串交出去。

func (*OSSCommand) Render added in v1.12.0

func (c *OSSCommand) Render() (string, error)

Render 是 ParseOSSCommand 的逆函数(TestOSSCommand_RoundTrip 锁住),也是审批弹窗与 审计行看到的展示态(决策 D6:规范形式是 DSL 串,匹配用派生出的策略串)。

拒绝以 "--" 开头的 Target,理由与 KafkaCommand.Render / MongoCommand.Render 完全一致: cmdline.Parse 判断 positional 还是 flag,看的是**剥掉引号之后**的词是否以 "--" 开头, 判断发生在引号信息已经丢失之后,所以加引号救不了。ParseOSSCommand 自己永远产不出 这种 Target,但 OSSCommand 是导出结构体,统一 exec 会用结构化的工具参数直接构造它、 绕开 Parse——拒绝好过悄悄产出一个 Render 完语义就变了的串。flag 名同理,判定复用 cmdline.ValidFlagName。

type PlannedTransferSource added in v1.12.0

type PlannedTransferSource struct {
	Path    string
	Expands bool
	ReadDir Direction
}

PlannedTransferSource is the protocol-neutral shape of one cp source.

type RedisClientCache

type RedisClientCache = KeyedConnCache[redisConnKey, redis.UniversalClient]

RedisClientCache 在同一次 AI Send 中复用 Redis 连接,按「资产 + 库」区分。

func NewRedisClientCache

func NewRedisClientCache() *RedisClientCache

NewRedisClientCache 创建 Redis 连接缓存

type RedisNodeRequiredError added in v1.15.0

type RedisNodeRequiredError struct {
	Command string
	Scope   string
	Masters []string
}

RedisNodeRequiredError 表示集群模式下一条不带 key 的命令没有指定有效节点:scope 缺省、 不是集群中的节点,或是一个库号。Masters 为当前主节点地址,供调用方(AI、opsctl、 桌面控制台)提示用户从中选择。用 errors.As 识别。

func (*RedisNodeRequiredError) Error added in v1.15.0

func (e *RedisNodeRequiredError) Error() string

type ResolveAssetFunc added in v1.12.0

type ResolveAssetFunc func(ctx context.Context, ref string) (*asset_entity.Asset, error)

ResolveAssetFunc 把 "<asset>:" 前缀解析成资产。由调用方注入:**共享的是语法,不是引用 解析策略**——opsctl 传自己的 resolveAsset(`cmd/opsctl/command/resolve.go`,支持 "组路径/名称" 消歧),AI 侧传 assetref.Resolve(数字 id 或精确名称)。两者在解析器上提 之前就已经不同,共享解析器不能替入口选一个。

解析不出资产时返回 error;ParseTransferEndpoint 只看 error 是否为 nil,不解读它的种类。

type SFTPClientCache added in v1.12.0

type SFTPClientCache struct {
	// contains filtered or unexported fields
}

SFTPClientCache 在一次 cp 命令内按资产复用 SFTP 会话。

func EnsureSFTPClientCache added in v1.12.0

func EnsureSFTPClientCache(ctx context.Context) (context.Context, *SFTPClientCache, bool)

EnsureSFTPClientCache 复用已有 cache,或创建一个由调用方负责关闭的新 cache。 第三个返回值说明本次调用是否拥有新 cache。

func NewSFTPClientCache added in v1.12.0

func NewSFTPClientCache() *SFTPClientCache

NewSFTPClientCache 创建使用真实资产连接的任务级 SFTP cache。

func (*SFTPClientCache) Close added in v1.12.0

func (c *SFTPClientCache) Close() error

type SSHClientCache added in v1.11.2

type SSHClientCache = ConnCache[*ssh.Client]

SSHClientCache 在同一次 AI Send 中复用 SSH 连接。

func NewSSHClientCache added in v1.11.2

func NewSSHClientCache() *SSHClientCache

NewSSHClientCache 创建 SSH 客户端缓存。

type SSHClientSet added in v1.14.0

type SSHClientSet struct {
	// contains filtered or unexported fields
}

SSHClientSet 在一次调用(如 opsctl batch)内按资产至多拨号一次:并发请求等待同一次 拨号,拨号失败(含 MFA 未通过 / 取消)被记住,后续请求直接拿到同一错误,不再对 同一资产发起第二次挑战。与 ConnCache 的区别正在于记住失败:一次性验证码不能重试。

func NewSSHClientSet added in v1.14.0

func NewSSHClientSet() *SSHClientSet

NewSSHClientSet 创建空集合;调用方负责 Close。

func (*SSHClientSet) Close added in v1.14.0

func (s *SSHClientSet) Close()

Close 关闭集合里全部已建立的连接。

type TransferAdapter added in v1.12.0

type TransferAdapter interface {
	// List 展开 pattern。pattern 含 glob 元字符(* ? [)时按 glob 展开;recursive 为真时
	// 下钻目录树;单个具体文件/对象返回单元素切片。展开途中遇到的符号链接一律跳过并计入
	// SkippedSymlinks——跟随会让 `cp -r ./dir` 因为一条指向 / 的链接变成整机 dump。
	// 指名到一个目录却没有 recursive 时报错,不猜。
	List(ctx context.Context, asset *asset_entity.Asset, pattern string, recursive bool) (*ListResult, error)
	// OpenRead 打开该端点上的 path 用于读取;size 未知时返回 -1。返回的 ReadCloser 活过
	// 本次调用,调用方负责 Close。
	OpenRead(ctx context.Context, asset *asset_entity.Asset, path string) (io.ReadCloser, int64, error)
	// Write 把 r 的内容写入该端点的 path,必要时创建中间目录;size 未知时传 -1。
	Write(ctx context.Context, asset *asset_entity.Asset, path string, r io.Reader, size int64) error
	// ValidateDestination 判断 path 能不能当这一端点的写入目标,只看形态、不碰网络。
	//
	// 它存在是因为目的端与源端不对称:源端的形态错误由 List 在展开时报出来,而目的端
	// **不经过 List**(要写的东西还不存在),ApprovalSubject 又不能报错。没有这一关,
	// 一个形态错误的目的地会带着一个错误的主体走完审批、到 Write 才失败——用户批准了
	// 一件必然失败的事。与 handleExec 的排序不变式同源:无副作用的判断必须全部走完,
	// 才能碰有副作用的那一步(审批弹窗)。
	//
	// 入参是**具体的**目的路径,即多源展开时 dst 基点拼上 RelPath 之后的结果——正是
	// 会被审批、也会被写入的那个字符串,因此这里不需要知道是单源还是多源。
	ValidateDestination(path string) error
	// ApprovalSubject 返回这一端点在该方向上必须被授权的审批类型与匹配串。
	//
	// DirList 方向收到的是用户**指名的那个串**——可能是一个 pattern,也可能是一个前缀,
	// 与 List 收到的完全相同。收窄成"实际会被枚举的那个基点"是适配器自己的事,调用方
	// 不得先替它截一刀:哪些字符是通配语法只有适配器知道(对象存储的前缀形态 key 里
	// `*?[` 是字面量,规则侧走的是 strings.HasPrefix),入口层按 glob 截断会让指名一个
	// 前缀的递归传输换来整桶列举的授权。反过来,主体也绝不能是 pattern 原文:cp 的 grant
	// 不分方向,一条 "/var/log/*.log" 的 grant 会连它命中的每个文件的读写一起授权。
	//
	// **一个端点有审批主体,当且仅当它有资产。** 本地端点没有资产,因此正确的调用方按
	// asset 是否为 nil 来决定要不要走权限检查,压根不会问本地适配器要主体;它返回的空串是
	// "不适用",不是一个让调用方去判空的哨兵——按哨兵写,漏判时就会静默放行。
	ApprovalSubject(path string, dir Direction) (approvalType, subject string)
}

TransferAdapter 是一类传输端点的全部能力:怎么展开、怎么读写、以及在每个方向上要授权 什么。四件事放在同一个接口里,是为了不出现第二张"类型 → 审批语义"的表。

func TransferAdapterFor added in v1.12.0

func TransferAdapterFor(asset *asset_entity.Asset) (TransferAdapter, error)

TransferAdapterFor 返回该端点的适配器,与 ParseTransferEndpoint 的返回值配套: asset 为 nil 就是本地端点——它没有资产,因此不进注册表,由包级实现直接交出。 没有接进传输面的资产类型返回错误,而不是回落成本地。

type TransferApprovalPlanner added in v1.12.0

type TransferApprovalPlanner interface {
	ApprovalSubjects(path string, dir Direction) []ApprovalTarget
}

TransferApprovalPlanner 是适配器的可选多主体授权能力。

type TransferCapabilities added in v1.12.0

type TransferCapabilities interface {
	SameAssetCopyHint(assetName string) string
}

TransferCapabilities 让入口询问适配器能力,不按资产类型或协议分支。

type TransferPathNormalizer added in v1.12.0

type TransferPathNormalizer interface {
	NormalizeTransferPath(path string) string
}

TransferPathNormalizer 是文件系统类适配器的可选能力。对象存储 key 是不透明名字,不能 用文件系统规则清理;本地与 SSH 则必须在审批、审计和 I/O 分叉之前归一成同一个路径。

type TransferPathResolver added in v1.12.0

type TransferPathResolver interface {
	ResolveTransferPath(ctx context.Context, asset *asset_entity.Asset, path string) (string, error)
}

TransferPathResolver 把端点专属的路径缩写解析为审批和 I/O 共用的确定路径。 当前只有 SSH 的 "~" / "~/..." 需要这一步;对象存储 key 不具有 home 语义。

type TransferPlan added in v1.12.0

type TransferPlan struct {
	Sources  []PlannedTransferSource
	Multiple bool
}

TransferPlan owns the shape rules shared by the AI and opsctl entrances.

func PlanTransfer added in v1.12.0

func PlanTransfer(sourcePaths []string, destination string, localDestination, recursive bool) (*TransferPlan, error)

PlanTransfer 判定传输形态。localDestination 表示目的端是本机路径:多源时目的端必须写成 目录,远端(POSIX 路径)以 "/" 结尾,本机路径还可以用本机的路径分隔符结尾(Windows 的 "\")。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址