乐于分享
好东西不私藏

pi agent 插件开发实录:读了 Anthropic 24 篇 agent 设计博客,我重构了 plan mode

pi agent 插件开发实录:读了 Anthropic 24 篇 agent 设计博客,我重构了 plan mode

pi agent 插件开发实录:读了 Anthropic 24 篇 agent 设计博客,我重构了 plan mode 插件

从 596 行 if/else 到 12 个 capability,一篇完整的技术拆解

───

上周干了一件挺折腾的事。

把 pi agent 上挂着的 plan mode 插件整个推倒重写了。

事情是这样的。

───

我之前的 plan mode 插件是基于一个 npm 包的,叫 @narumitw/pi-plan-mode。它里头有个 isSafeCommand 函数,596 行 if/else。我用了大半年,最近越用越觉得不对劲。

最明显的一个例子。我有个 agent 在只读 plan 模式下调研一个外部 API,它跑了一条 curl 命令。

curl https://api.github.com/users/octocat

没问题,GET 请求,只读操作。

然后它想看看那个 API 的 POST endpoint 长啥样,写了

curl -X POST -d '{"foo":"bar"}' https://api.example.com/webhook

通过了。

我当时就愣住了。。。

POST 请求?写操作?这也能过?

我回去翻了一下那个 596 行的 isSafeCommand。它对 curl 的处理就一行,curl 命令前缀通过。但 curl 能传 -X POST,能传 -d data,能传 --upload-file。这些它全不管。

我当时就觉得,这个东西维护起来不是人过的日子。

───

另一个更吓我的。

就在我研究 plan mode 实现的时候,顺手翻了下官方 example。

然后我发现了一个 bug,挺恶心那种。

官方 plan mode 的代码里,在 event.messages.filter(...) 的回调里有这么一段

if (msg.content.includes("[PLAN MODE ACTIVE]")) return false;

对,就是在 user message 里找「PLAN MODE ACTIVE」这个字符串,找到了就把这条消息删掉。

我当时一脸问号???

这个过滤的意图我大概能猜,开发者可能是想过滤掉某种 prompt injection 或者重复消息。但用字符串字面过滤做安全边界,这特么不是 bug 吗?

任何用户只要在 message 里写了「我们现在不在 PLAN MODE ACTIVE 模式」这种话,那部分历史就被吞了。

更糟的是,agent 自己只要在回复里写了「PLAN MODE ACTIVE」字样(甚至在解释自己的状态),相关的对话上下文也会被影响。

同一段代码还过滤掉了 customType === 'plan-mode-context' 的消息。双重过滤机制叠加在一起,碰到任何意外字符串组合都可能误删历史。

我看完这段代码,心里就一个想法,这玩意不能再用了。

───

我读了 Anthropic 24 篇工程博客

正好这段时间我把 Anthropic 从 2024 年到 2026 年发的 24 篇工程博客都啃了一遍。其中至少 6 篇直接跟权限控制、agent 安全、长任务执行相关,是 agent 设计领域的硬菜。

有几篇对我冲击特别大。

「How we built Claude Code auto mode」。讲他们怎么把权限判断从「一刀切 allow/deny」改成三层架构。第一层是 safe tool 白名单,比如读文件、文本搜索这些无脑放行的工具。第二层是项目内文件操作,自动放行不弹确认。第三层才是真正的 classifier,对可能有副作用的操作做判断。

而且他们反复强调一件事,prompt 不能用 if/else 硬规则。他们叫这个「Goldilocks altitude」,既不能太具体(brittle),也不能太模糊(agent 不知道怎么做),要刚刚好。

他们实测下来的数据,让我印象很深。合理的「自动放行 + 智能拦截」组合能让权限弹窗数量减少 84%,用户对弹窗的接受率反而提升到 93%。

「Effective harnesses for long-running agents」。专门讲长任务怎么不让 agent 卡住或者中断。核心观点是,harness 的设计应该把「上下文续传」「任务拆分」「checkpoint」这些做扎实,而不是指望 agent 自己记得所有事。

「How we built our multi-agent research system」。讲他们怎么让一个研究任务跑 30 分钟不中断,关键是他们给子 agent 的权限边界是预先定义好的,子 agent 在自己的研究领域里几乎不会被弹窗打断。

我当时看完这几篇,心里就有数了。

我的 plan mode 插件不能用 if/else 硬规则,得让 agent 知道自己的边界然后自己判断。权限应该是分层而不是一刀切。长任务跑 plan 模式调研不能被频繁中断。

这三点成了我重写 plan mode 插件的核心指南。

───

三件我从中学到的事

带着这三个 insight,我把 plan mode 插件整个重写了。新的版本不是技术升级,是三个产品决策。

一,yolo 模式,干掉烦人的权限审批

之前那个 npm 包的逻辑是「未识别的命令 → 默认拒绝 → 弹窗让用户确认」。听起来很安全对吧?

我实际用了大半年,最大感受是两个字,烦死。

为什么烦?因为命令组合永远穷举不完。git 有几十个子命令,gh 有几十个,npmcargodockerkubectlaws 都有自己的子命令树。每个组合都要单独配置安全规则,加一个新命令就要改代码。

更要命的是这种审批模式不区分场景。agent 在调研外部 API 时跑 curl -X POST 测试,那是个合理的探索行为,硬要弹窗就是打断心流。agent 在生产环境跑 rm -rf /,那才该拦截。

我这次干脆砍掉审批模式,就保留两个状态。Build 模式,啥都能干。Plan-Yolo 模式,只读 + HTTP GET。Tab 键切一次进 plan,再切一次回 build。

砍掉之后我自己用了两周,体验是这样的。没有任何命令被弹窗打断。碰到真正需要写的操作,agent 通过 plan_commit 工具提交一个计划给我看,我审批或者驳回。整条链路里没有「你确定要执行这个吗」这种废话。

背后的实现细节是这样的。我把命令分类从 if/else 换成了 capability 标签。

先定义一套 capability。我用了一个 12 个元素的 union 类型

type Capability =  | "read:filesystem"  | "write:filesystem"  | "read:process"     | "write:process"  | "read:network"     | "write:network"  | "read:git"         | "write:git"  | "read:package"     | "write:package"  | "execute:subprocess"  | "modify:environment";

读文件系统、写文件系统、读进程、写进程、读网络、写网络、读 git 状态、写 git 操作、读包信息、写包信息、执行子进程、修改环境变量。一共 12 个 capability,覆盖了大多数 bash 命令的能力维度。

Plan-Yolo 模式默认只允许 5 个只读 capability

const PLAN_DEFAULT_ALLOWED = new Set([  "read:filesystem",  "read:process",  "read:network",  "read:git",  "read:package",]);

任何命令,只要触发 write:* / execute:subprocess / modify:environment 里的任何一个,就被拦截。读 + GET 网络操作全放行。

每条命令我给它写一个 profile。profile 长这样

interface CommandProfile {  base: Capability[];   // 基础能力  forbid?: Record<string, Capability>;  // 触发额外能力的参数  dynamic?: (subcommand: string) => CommandProfile | null;  // 子命令动态 profile}

举几个真实的例子。

curl 的 profile

curl: {  base: ["read:network"],  forbid: {    "-X": "write:network",    "-d""write:network",    "--data""write:network",    "--upload-file""write:filesystem",    "-o""write:filesystem",    "--output""write:filesystem",    "-O""write:filesystem",  },},

curl 基础是读网络。但 -X POST / -d data 这些参数会触发写网络,-o file / --upload-file 触发写文件系统。

这里有一个挺关键的细节。curl URL 不带 -X 时默认是 GET(业界惯例),所以不加 write:network。但如果用户显式写了 curl -X GET,我的引擎会在参数扫描时识别 GET/HEAD/OPTIONS,覆盖掉 -X 触发的 write:network。这样既支持 curl URL(隐式 GET),也支持 curl -X GET URL(显式 GET),两种都放行。

find 的 profile

find: {  base: ["read:filesystem"],  forbid: {    "-exec": "execute:subprocess",    "-execdir""execute:subprocess",    "-ok""execute:subprocess",    "-okdir""execute:subprocess",    "-fprint""write:filesystem",    "-fprint0""write:filesystem",    "-delete""write:filesystem",  },},

find . -name "*.txt" 默认放行,是只读操作。但 find . -exec rm {} ; 触发 execute:subprocess,被拦截。find . -fprint output.txt 触发 write:filesystem,被拦截。

加新命令就是加一条 profile,10 行搞定。不用再翻 600 行 if/else。这就是 declarative 模式的核心好处。

引擎判断流程分三步。

第一步,parseShellWords 做词法分析。把 shell 命令切成 token 数组,正确处理单引号、双引号、反斜杠转义。

第二步,splitShellSegments 按 ;&|&&|| 分割命令链。一个 ls | grep foo 被切成两个 segment,分别判断。每个 segment 都得是允许的,整条命令才放行。

第三步,classifyArgs 走 profile 表。基础能力从 profile.base 取,再扫一遍参数看有没有触发 profile.forbid 里的额外 capability。未知命令(不在 COMMAND_PROFILES 里)默认加全部 write:* capability,等于直接 deny。

整个引擎 510 行代码,对比之前 npm 包的 596 行,代码量差不多,但可读性和可扩展性完全不在一个量级。

二,尽可能长完成任务,不打断探索

之前的 plan mode 经常把一个完整的调研任务切成好几段。

举个具体例子。agent 要调研一个 npm 包,跑了 npm view xxx 查版本,然后 npm pack xxx --dry-run 看里面有什么文件,再 curl -X GET https://registry.npmjs.org/xxx 看 metadata。

这三个命令都被拦了,理由是「npm pack 不在白名单 / curl 带了 -X 参数 / 等等需要审批」。

结果就是,agent 跑两步就被拦截一次,切回 build 模式继续,再切回 plan 模式调研。一个完整的调研任务被打散成四五段,每段切回来都要重新组织上下文,心流全断。

Anthropic 在 multi-agent 那篇文章里讲了一个观点,特别打动我。他们说,子 agent 在自己的研究领域里应该几乎不会被弹窗打断。因为频繁打断会让 agent 反复重读上下文、反复重做已经做完的事,效率损失远比「偶尔放过一个边角操作」大。

我这次的设计思路就是,能放行的尽量放行。

Plan-Yolo 模式下,agent 能看到的工具列表是这样的

const PLAN_TOOLS = [  "read""bash""grep""find""ls",  "questionnaire""plan_commit",as const;

Build 模式的工具列表是

const BUILD_TOOLS = [  "read""bash""edit""write""grep""find""ls",as const;

注意两个关键差异。

第一,Plan-Yolo 模式完全移除了 edit 和 write。LLM 看不到这两个工具,它根本不会尝试去调用。这是从工具列表层面做的硬约束,比 bash 命令拦截更彻底。

第二,Plan-Yolo 模式多了 questionnaire 和 plan_commit。这两个是我自己注册的工具。

plan_commit 是 commit 仪式。agent 在 Plan-Yolo 模式下做了足够调研之后,调用这个工具提交计划。工具长这样

pi.registerTool({  name: "plan_commit",  parameters: Type.Object({    plan: Type.String({ minLength: 1, maxLength: 50_000 }),  }),  execute: async (params) => ({    content: [{      type: "text",      text: `**Proposed Plan**\n\n${params.plan}`,    }],    details: { plan: params.plan },    terminate: true,  }),});

调用时返回一个 **Proposed Plan** 的 markdown 块,并且 terminate: true 强制终止当前 turn,等用户审阅。50_000 字符上限是参考 Anthropic 官方 plan_mode_complete 工具的设计定的,避免 agent 一口气写个万字 plan 把上下文撑爆。

questionnaire 是提问工具。这个我是直接从 @earendil-works/pi-coding-agent/examples/extensions/questionnaire.ts 复制的,448 行代码。一个工具同时支持单问题模式和多问题模式,单问题走选项列表,多问题走 tab 界面。这个设计挺优雅的,省去了注册两个不同工具的麻烦。

借鉴了 opencode 的思路。opencode 的 plan agent 把 edit 和 bash 都设成 ask 模式,但他们同时给 plan agent 大量预设的「自动放行」操作。opencode 不去对每个命令分类,而是对「操作类型」分类,类型对了就放行。

我把这个思路翻译到自己的设计里。Plan-Yolo 模式下,「读取类操作」「HTTP GET 类操作」「只读命令类操作」自动放行。只有当 agent 试图做「写文件」「执行脚本」「POST/PUT/DELETE」时,引擎才介入判断。

具体来说,agent 调研外部 API 时可以一路读到底,不用切来切去。

切换模式的实现是这样的。我用一个状态机来追踪当前模式

// state.tsinterface PlanModeState {  enabledboolean;  enteredAtnumber;}export function isPlanModeEnabled(ctx): boolean { ... }export function readState(ctx): PlanModeState { ... }export function writeState(pi, statePlanModeState): void {  pi.appendEntry<PlanModeState>("plan-mode-state", state);}

状态持久化通过 pi.appendEntry 写到 session branch entry 里。每次切换模式就追加一条 customType === 'plan-mode-state' 的 entry。读取时反向扫描取最后一条(last-write-wins)。

切换的实现

// extension.tsasync function togglePlanMode(pi, ctx) {  if (isPlanModeEnabled(ctx)) {    // Exit plan → restore build tools    pi.setActiveTools(savedToolsBeforePlan ?? BUILD_TOOLS);    writeState(pi, { enabledfalseenteredAt0 });  } else {    // Enter plan → switch to plan tools    savedToolsBeforePlan = pi.getAllTools()      .filter(t => t.sourceInfo?.source === 'builtin')      .map(t => t.name);    pi.setActiveTools(PLAN_TOOLS);    writeState(pi, { enabledtrueenteredAtDate.now() });  }}

pi.setActiveTools 是 pi-coding-agent 提供的 API,直接替换 LLM 当前能看到的工具集。savedToolsBeforePlan 保存进入 plan 之前的工具列表,退出时恢复。

三,Tab 切换 plan / build,简单好用

这个直接借鉴自 opencode。

opencode 的 plan agent 是个独立的 agent profile,有自己的权限边界和工具集。按 Tab 键切换 build 和 plan 两种模式,build 是默认的全权限 agent,plan 是只读权限的探索 agent。

参照 opencode 的 Tab 方便快捷,我也搬过来——两档。Build ↔ Plan-Yolo。

按 Tab 进 plan,再按 Tab 回 build。简单粗暴。

具体注册 Tab 键是这样

// extension.tspi.registerShortcut("tab", {  description"Toggle plan mode",  handlerasync (ctx) => togglePlanMode(pi, ctx),});

也支持 /plan-toggle slash 命令注册,两条路径走同一个 handler。

整个 plan mode 我拆成了一个独立的 src/plan-mode/ 目录。settings.json 里只注册一行

{  "packages": [    "src/plan-mode/index.ts"  ]}

src/plan-mode/ 下 7 个文件

state.ts          ← 状态机(40 行)bash-policy.ts    ← capability 命令分类器(510 行)bash-policy.test.ts ← 61 个测试用例(165 行)prompt.ts         ← Goldilocks altitude 系统提示(58 行)questionnaire.ts  ← 自注册提问工具(448 行,复制自官方 example)plan-commit.ts    ← plan_commit 工具(78 行)index.ts          ← 主入口,组合上述模块(170 行)

整个 plan mode 是完全独立、零 npm 依赖的模块。

依赖清理也很干脆

pi uninstall npm:@narumitw/pi-plan-mode# removed 2 packages

settings.json 里这个 npm 包彻底删了。本机不再依赖 @narumitw/pi-plan-mode,整个 plan mode 从能力引擎到 commit 仪式全是我自己写的。

───

四,prompt 怎么写,一段 Goldilocks altitude 的拆解

讲完三个产品决策,得回到一个最基础的问题。这套 yolo 模式,agent 在跑的时候到底知道什么?

我注入了一段 58 行的系统提示,叫 PLAN_PROMPT_YOLO。分五个区块。

第一个区块,「Available tools」,直白告诉 agent 现在能用 read、grep、find、ls、bash、questionnaire、plan_commit 这七个,edit 和 write 主动不可见。

这块设计上的关键不是「列工具名」,是「明确告诉 agent 哪些工具不存在」。如果只列允许的,agent 会去找 edit 找不到,可能误以为是自己权限不够。如果直接说「edit 和 write 不在列表里」,agent 就不会瞎找,体验顺得多。

第二个区块,「Allowed bash」。这块是真正体现 Goldilocks altitude 的地方。

我列出了 5 个默认允许的 capability,read:filesystem、read:process、read:network、read:git、read:package。然后列了 3 个默认禁止的 capability 类别,write 开头的一切、execute:subprocess、modify:environment。

但我没有列具体的命令。

这就是「既不具体也不模糊」的中间地带。我没说「curl 允许,wget 允许,POST 禁止」(太具体,太脆)。我也没说「agent 自己看着办」(太模糊,agent 不知道边界在哪)。

我说「read:network 允许,write:network 禁止」。

agent 拿到这个提示,自己去推。curl 默认 GET 是 read:network,POST 是 write:network。git 默认 log 是 read:git,commit 是 write:git。这套 reasoning agent 完全能搞定。

第三个区块,「How to behave」,四条行动指南。

第一条「Explore widely」,告诉 agent 调研的时候大胆读文件、跑只读命令、做 GET 请求。这条是反向设计的,因为之前那个 npm 包给了太严的边界,agent 调研两步就被打断一次,反而效率低。

第二条「Ask when uncertain」,引导 agent 用 questionnaire 问问题。明确写「不要问能从文件里读到的东西」,避免 agent 拿问卷套话。

第三条「Synthesize at the end」,提示 agent 调研够了就整合成 Markdown plan,然后用 plan_commit 提交。

第四条「Do not attempt to write files」,明确禁止在 plan 模式调用 edit 和 write 这两个工具。这条是兜底,因为 prompt 里写「tool 不可用」和「行为上不要尝试调用」是两回事。前者靠工具列表硬切,后者靠 agent 自己理解。

第四个区块,「When a bash command is blocked」。这条是给 agent 解释拦截逻辑的。

直接说「不要尝试绕过安全层」。原文是「no POST disguised as GET, no shell tricks」,翻译过来就是别用奇怪的方法绕我的判断。

然后给两条出路。一是找只读替代方案(如果就是想读数据,那一定有种只读的方式),二是写到 plan 里描述操作,让用户切回 build 模式执行。

这种「给路径但不给绕过」的措辞是关键。安抚了 agent「被拦了不要慌」,但坚决不让它动绕过的念头。

第五个区块,「When to commit」,调用 plan_commit 的三个条件。

不是「TBD」还有 → 不调。重大决策没问用户 → 不调。plan body 不能独立站住 → 不调。

这三个条件把「我准备好了」的判断标准从主观感觉变成了客观清单。agent 自己也知道什么时候该提交。

写完回头看,这 58 行 prompt 的核心就是一个判断,「用能力边界描述代替命令白名单」。前者是声明式的,agent 自己推;后者是命令式的,agent 只能照做。

跟 Anthropic 那个 template-based classifier prompt 是同一个思路。固定结构 + 几个可配置 slot,其他都是变量。

───

五,bash-policy 引擎怎么搭起来

prompt 解决的是「agent 怎么想」,bash-policy 解决的是「bash 命令怎么判」。这俩是配对的,prompt 给了 agent 边界感,引擎负责把边界感落地成机器执行。

整个引擎 613 行 TypeScript,分四块。

第一块,Capability 定义。12 个元素的 union 类型。

read:filesystemwrite:filesystemread:processwrite:processread:networkwrite:networkread:gitwrite:gitread:packagewrite:packageexecute:subprocessmodify:environment

为什么是这 12 个,不是 8 个也不是 20 个?

我是从「4 个资源 × 2 个动作」推导的。资源是 filesystem、process、network、git、package、environment 这几类 bash 命令实际能影响的对象。动作是 read(查询)和 write(修改)。

但 environment 只有 write 没有 read,因为 bash 命令查询环境变量本质是读 filesystem 的环境块,没有独立的 read:environment 需求。

execute:subprocess 是单拎出来的,因为有些命令自身只 read,但能 fork 别的进程(find -exec、bash -c),这是独立的能力维度,必须单独标注。

这一套推导下来,12 个刚好覆盖所有 bash 命令的能力维度,没有重叠也没有漏。

第二块,COMMAND_PROFILES。这是引擎的核心数据,70 多条命令的 profile 表。

profile 长这样

interface CommandProfile {  base: Capability[];   // 命令基础能力  forbid?: Record<string, Capability>;  // 触发额外能力的参数  dynamic?: (subcommand: string) => CommandProfile | null;  // 子命令动态 profile}

base 是命令本身的能力。forbid 是参数扫描,碰到特定 flag 就升级能力。dynamic 是子命令动态判断,专门给 git、npm、yarn、pnpm 这种子命令区分读写的命令用。

举三个具体例子。

cat 的 profile 一行就写完,{ base: ["read:filesystem"] }。因为 cat 没有写模式参数,profile 就这么简单。

curl 的 profile 复杂一点。base 是 read:network,但 forbid 里有 14 个 flag 映射到 write:network 或 write:filesystem。比如 -d--data--data-raw--data-binary--data-urlencode-F--form-X(除了显式 GET/HEAD/OPTIONS)、--request 全部触发 write:network。-o-O--output--remote-name-T--upload-file 触发 write:filesystem。

git 的 profile 最特殊。base 是 read:git,但 dynamic 函数根据子命令返回不同 profile。

子命令在「READ_ONLY set」里(log、diff、show、status、branch、remote、config、ls-files、ls-tree、ls-remote、rev-parse、blame、describe、merge-base、cat-file、grep、shortlog、reflog、tag、stash、fetch、archive 等 20+ 个),返回 { base: ["read:git", "read:network"] }

其他子命令(commit、push、add、checkout、reset、merge、rebase 等等),返回 { base: ["write:git", "read:filesystem"] }

这里 read:network 加给 fetch、clone 等只读网络操作的命令,read:filesystem 加给 commit、push 等需要读工作区的命令。

第三块,shell 解析器。三个独立函数。

parseShellWords 做词法分析。把一行 shell 命令切成 token 数组。正确处理单引号、双引号、反斜杠转义。引号不平衡或转义不合法返回 null,调用方按「解析失败 → 拒绝」处理。

举几个 edge case。

echo "hello world" → ["echo", "hello world"],引号内的空格不算分隔符。

echo 'it''s ok' → ["echo", "its ok"],两段单引号拼接。

echo "a\"b" → ["echo", "a"b"],反斜杠在双引号内转义下一个字符。

失衡引号 echo "hello → 返回 null。

splitShellSegments 按 shell 分隔符切割。一行命令可能用 ;&|&&|| 串起来。

ls /home | grep foo → ["ls /home", "grep foo"],两个段各自独立判定。

引号内的分隔符不切割。echo "a;b" | grep a → ["echo \"a;b\"", "grep a"]

containsWriteRedirect 检测输出重定向。>>> 在引号外算重定向,引号内不算。

echo > /dev/null → true(重定向)。echo ">" file → false(> 在引号内)。

注意这条有个 bug 我发现得比较晚,stderr 重定向 2>/dev/null 也算重定向,会触发 write:filesystem。这个保守策略跟我的 deny-by-default 原则一致,宁可误拦不要漏放。

第四块,入口判定函数 isCommandAllowed

function isCommandAllowed(command, allowed = PLAN_DEFAULT_ALLOWED): boolean {  const trimmed = command.trim();  if (!trimmed) return false;  const segments = splitShellSegments(trimmed);  if (!segments) return false;  for (const seg of segments) {    if (!seg) continue;    const args = parseShellWords(seg);    if (!args || args.length === 0return false;    const hasRedirect = containsWriteRedirect(seg);    const caps = classifyArgs(args);    if (!caps) return false;    if (hasRedirect) caps.add("write:filesystem");    for (const cap of caps) {      if (!allowed.has(cap)) return false;    }  }  return true;}

核心就是这 5 步流水线。

trim → split → parse → classify → check。每一层失败都直接 false(deny-by-default)。

deny-by-default 的三层防线。

第一层是「未识别命令 → 默认 deny」。classifyArgs 遇到不在 COMMAND_PROFILES 里的命令返回 null,引擎直接 false。

第二层是「任何 forbid 命中 → 加 cap」。参数扫描碰到 forbid 里的 flag 就往 caps 里加。哪怕 base 都在 allowed 里,加进来的 cap 不在 allowed 里也 false。

第三层是「段内任一 cap 不允许 → 整段 false」。一个段有 read:network(允许)和 write:filesystem(不允许),整段 false。

这三层防线的好处是规则简单。每条都是「不满足就拒绝」,不需要复杂的 reasoning。这就是 declarative vs imperative 的本质区别,命令不是被推理判断的,是被查询匹配的。

写到这里回头看,整个引擎最让我满意的设计是 COMMAND_PROFILES 表。它不是代码,是数据。

加新命令是加一条数据,不是改核心代码。改错了最多那条命令行为不对,不会波及其他命令。删命令是删一条数据,不会破坏引擎逻辑。

对比之前 npm 包那个 596 行 if/else,加新命令要在 if/else 链里加 case,改错了可能破坏既有判断,还得加测试覆盖。

这就是 declarative 模式的工程价值,也是我为什么觉得这种设计是「足够好、能维护、能演进」的方案。

───

实战踩坑:3 个发现的 bug 和修复

整个开发过程中我自己发现了 3 个 bug,挺有意思。

Bug 1: resolveProfile 在 stripGlobalFlags 之前执行

第一次写的时候,resolveProfile(cmd, args) 接收的是原始 args。对于 git -C /repo log,我直接把第一个非 flag 参数当作 subcommand。结果是 subcommand = "-C",触发了 dynamic profile 解析失败。

修复方案是把 resolveProfile 拆成两步

function resolveProfile(cmd, args): CommandProfile | null {  // 1. 静态 profile(不依赖 subcommand)  const staticProfile = COMMAND_PROFILES[cmd] ?? null;  if (!staticProfile) return null;  if (!staticProfile.dynamicreturn staticProfile;  // 2. 动态 profile(依赖 subcommand,subcommand 要从 stripGlobalFlags 之后的 args 取)  const cleanedArgs = stripGlobalFlags(args);  const subcommand = cleanedArgs[0];  return staticProfile.dynamic(subcommand);}

stripGlobalFlags 把 -C / -c / --git-dir / --work-tree / --namespace / --no-pager 这些 git 全局 flag 剥掉,剩下的第一个 token 才是真正的子命令。

Bug 2: containsWriteRedirect 短路拦截

判断 shell 命令里有没有 > / >> 重定向时,我一开始写的是短路逻辑

if (containsWriteRedirect(segment)) {  return { allowed: false, reason: "contains redirect" };}

问题来了。即使 allowed caps 包含 write:filesystem,短路逻辑也会拦截 echo hello > file。这意味着默认 Plan-Yolo 模式(不允许 write:filesystem)会拦截,但用户自定义的扩展模式(允许 write:filesystem)也会拦截。

修复方案是把短路换成 cap 检查

const caps = classifyArgs(cmd, args);if (containsWriteRedirect(segment)) {  caps.add("write:filesystem");}if (!isCommandAllowed(caps, allowedCaps)) {  return { allowed: false, reason: formatBlockReason(cmd, caps) };}

让重定向往 caps 里加 write:filesystem,再让正常的 cap 检查决定放不放行。这样如果用户配置了允许 write:filesystem,重定向就放行;否则拦截。

Bug 3: find -exec 内嵌命令静态分析

find . -exec rm {} ; 这种命令,我的引擎只能识别出 find 触发 execute:subprocess,无法静态分析 -exec 后面的 rm。这是个保守策略。理想方案是解析 -exec 后面的命令再走一遍 profile 表查 rm,但这复杂度爆炸,我没做。

折中方案是 execute:subprocess 直接进 deny set。Plan-Yolo 模式默认不允许 execute:subprocess,任何 -exec / --exec / bash -c 都拦。要用就得切回 build 模式。

───

测试覆盖:61 个 case

bash-policy.test.ts 跑下来 61 个 assertions 全过。覆盖了四个层面。

词法解析(10 个 case)parseShellWords 处理 cmd "arg with space" / cmd 'arg' / cmd arg\ escaped / 失衡引号返回 null。splitShellSegments 处理 cmd1; cmd2 / cmd1 && cmd2 / cmd1 || cmd2 / cmd1 | cmd2 以及引号内的特殊字符不分割。

重定向检测(4 个 case)echo > file 触发,echo ">" file 不触发(引号内的 > 不算),echo >> file 触发,echo 2>/dev/null 触发(stderr 重定向也是写)。

参数分类(13 个 case)。curl -X POST 触发 write:network,curl -X GET 不触发(覆盖默认),curl -d data 触发,curl -o file 触发 write:filesystem,curl -O 触发。find -exec 触发 execute:subprocess,find -delete 触发 write:filesystem。git log 静态走 read:git,git push 走 dynamic 触发 write:git。npm list 走 read:package,npm install 走 write:package

端到端命令检查(34 个 case)。25 个允许的命令(包括 cat file / git log / curl URL / npm view xxx / ls -la / grep pattern file / head -n 10 file),36 个应该被拦截的命令(包括 rm -rf / / curl -X POST URL / git push origin master / npm install xxx / bash script.sh / python3 -c "import os" / ssh user@host)。

跑测试很简单

npx -y tsx src/plan-mode/bash-policy.test.ts# ✓ All bash-policy assertions passed (25 allowed + 36 blocked cases)

61 个 case 一次性过,让我对 capability 引擎有信心。后续加新命令时,加一条 profile,跑一遍测试,不会破坏既有 case。

───

一点更深的思考

聊到这里,我突然想起一个软件工程里的老话题,declarative vs imperative。

我之前的那个 npm 包是 imperative 的,每条命令一个 if/else 分支,每加一条命令就要在 if/else 链里加一个 case。这跟写一堆 if/else 处理业务逻辑是一样的,时间长了就是 spaghetti code。

我新的方案是 declarative 的,每个命令一个 profile,声明它的能力。引擎只负责 evaluate,不负责判断逻辑。

这个区别其实挺大的。

imperative 的写法,加新规则 = 改核心代码。你得保证改对了,不能破坏既有逻辑,还得加测试覆盖。

declarative 的写法,加新规则 = 加一条数据。引擎不动,改错了最多那条命令行为不对,不会波及其他命令。

这几年我自己的代码风格也在往 declarative 转。能用配置表达的就不用代码,能用数据表达的就不用逻辑。这不是说 imperative 不好,有些场景 imperative 更直接。但安全策略这种需要长期维护、又怕出错的场景,declarative 真的香。

Anthropic 写权限系统的时候也是这个思路,他们 classifier 的 prompt 是 template-based 的,固定部分处理解释工作,可变部分只有三个 slot,「信任的环境」「拦截的类别」「例外情况」。这个设计的好处是,默认配置大概率是对的,用户想自定义也容易,不用动核心 prompt。

挺值得借鉴的。

───

收尾

坦率的讲,我自己这个版本也不完美。

HTTP 方法的判断还是有漏的可能。比如 curl -G --data-urlencode "x=y" URL,这个 -G 把 POST 转成 GET,但带了数据。我的引擎没处理这个 case。

还有 find -exec 内嵌的命令,我现在的策略是只要看到 -exec 就拦截,比较保守。但 find . -exec cat {} ; 这种无害的命令也被拒了。理想方案是分析 -exec 后面的命令,但这复杂度就上来了,我没做。

shell 的边界情况更多。> 重定向我处理了,但 <() 进程替换、$(...) 命令替换、heredoc,这些我的解析器都不支持。碰到就 deny。这是个保守但安全的策略。

但起码比 596 行 if/else 好维护。

我估计未来还会再迭代一轮,把 capability 引擎做得更精细。比如:

用户配置 via ~/.pi/agent/settings.json#plan-mode.allowedCapabilities,让用户能在不动代码的前提下调整 capability 白名单。
OS-level sandbox 集成(bubblewrap/seatbelt),从内核层做硬隔离,不依赖 shell 解析器。
plan_session 落盘,把 plan 写入 session 文件,下次启动能 resume。

但这次先把骨架立起来,核心安全边界和用户体验都有了,剩下的细节慢慢来。

有点感慨,软件工程这件事,没有银弹。每一代设计都在解决上一代的问题,又带来新的问题。重要的不是找到一个完美的方案,是找到一个「足够好、能维护、能演进」的方案,然后持续迭代。

我这次重写的 plan mode 插件就是这个思路。不追求完美,追求「让我能睡个好觉」。

以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~

谢谢你看你的文章,我们,下次再见。

───

🔗 项目地址:https://gitee.com/linbirg/pi-extentions.git

git clone https://gitee.com/linbirg/pi-extentions.git

作者:程序员学量化
基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-16 16:49:23 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/905751.html
  2. 运行时间 : 0.112258s [ 吞吐率:8.91req/s ] 内存消耗:4,836.23kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=ea4e16098d4140b8a416793e6d13b689
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 4.22 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.11 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000397s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000581s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000272s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000328s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000499s ]
  6. SELECT * FROM `set` [ RunTime:0.000233s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000572s ]
  8. SELECT * FROM `article` WHERE `id` = 905751 LIMIT 1 [ RunTime:0.001678s ]
  9. UPDATE `article` SET `lasttime` = 1786870163 WHERE `id` = 905751 [ RunTime:0.002699s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000292s ]
  11. SELECT * FROM `article` WHERE `id` < 905751 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000478s ]
  12. SELECT * FROM `article` WHERE `id` > 905751 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000461s ]
  13. SELECT * FROM `article` WHERE `id` < 905751 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000793s ]
  14. SELECT * FROM `article` WHERE `id` < 905751 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001068s ]
  15. SELECT * FROM `article` WHERE `id` < 905751 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.003162s ]
0.115456s